From b5d4cd1f0bf35a0537844a32237ca09b2902d2d3 Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Mon, 28 Sep 2026 16:59:06 +0200 Subject: [PATCH 001/145] :bug: Fix problems with react loops (#11941) --- .../app/main/data/workspace/libraries.cljs | 45 ++++++--- .../ui/workspace/colorpicker/libraries.cljs | 98 +++++++++---------- .../main/ui/workspace/colorpicker/ramp.cljs | 7 +- .../main/ui/workspace/sidebar/options.cljs | 42 ++++---- 4 files changed, 105 insertions(+), 87 deletions(-) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index fd2250c379..df0a514ca5 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -1448,12 +1448,17 @@ (rx/filter (complement :translation?)) ;; Keep waits pending while component changes are checked. (rx/map start-sync-barrier) - (rx/observe-on :async)) + (rx/share)) - get-component-events - (fn [[event old-data]] - (let [{:keys [file-id changes save-undo? undo-group]} event - changed-components + ;; Buffers commits until a timer turn passes with no new ones. The + ;; debounce timer (not a microtask) queues behind any timers already + ;; due, so a burst of commits is inspected as one batch in one task. + commit-batches-s + (rx/buffer-until (rx/debounce 0 commits-s) commits-s) + + get-component-changes + (fn [old-data {:keys [file-id changes save-undo? undo-group]}] + (let [changed-components (when (and old-data (or (nil? file-id) (= file-id (:id old-data)))) (into #{} @@ -1461,29 +1466,39 @@ changes))] (cond (empty? changed-components) - (rx/empty) + nil save-undo? (do (log/info :hint "detected component changes" :ids (map str changed-components) :undo-group undo-group) - (->> (rx/from changed-components) - (rx/map #(component-changed - % (:id old-data) undo-group)))) + (map #(vector ::component-changed % undo-group) + changed-components)) :else ;; Undos only bump :modified-at. - (->> (rx/from changed-components) - (rx/map touch-component))))) + (map #(vector ::touch-component %) changed-components)))) + + ;; One event per distinct component change in the batch + get-component-events + (fn [batch old-data] + (->> batch + (mapcat #(get-component-changes old-data (first %))) + (distinct) + (map (fn [[type component-id undo-group]] + (if (= type ::component-changed) + (component-changed component-id (:id old-data) undo-group) + (touch-component component-id)))) + (rx/from))) component-events-s - (->> commits-s + (->> commit-batches-s (rx/with-latest-from workspace-buffer-s) (rx/mapcat - (fn [[[event task] old-data]] - (->> (get-component-events [event old-data]) - (rx/finalize #(finish-sync-barrier! task))))) + (fn [[batch old-data]] + (->> (get-component-events batch old-data) + (rx/finalize #(run! (comp finish-sync-barrier! second) batch))))) ;; Close barriers left behind when the page shuts down. (rx/finalize #(wrf/finish-tasks! @pending-sync-barriers*)) (rx/share)) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs index 5e01c95785..d4ae459cbf 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/libraries.cljs @@ -202,12 +202,6 @@ file-id (mf/use-ctx ctx/current-file-id) - current-colors* (mf/use-state []) - current-colors (deref current-colors*) - - grouped-colors* (mf/use-state {}) - grouped-colors (deref grouped-colors*) - open-groups* (mf/use-state #{}) open-groups (deref open-groups*) @@ -221,13 +215,15 @@ [{:value "recent" :label (tr "workspace.libraries.colors.recent-colors") :id "recent"} {:value "file" :label (tr "workspace.libraries.colors.file-library") :id "file"}]) + ;; Equal-stable, since `select*` resets its selection when options change options - (mf/with-memo [library-options libraries file-id] - (into library-options - (comp - (map val) - (map (fn [lib] {:value (d/name (:id lib)) :label (:name lib) :id (d/name (:id lib))}))) - (dissoc libraries file-id))) + (h/use-equal-memo + (mf/with-memo [library-options libraries file-id] + (into library-options + (comp + (map val) + (map (fn [lib] {:value (d/name (:id lib)) :label (:name lib) :id (d/name (:id lib))}))) + (dissoc libraries file-id)))) on-library-change (mf/use-fn @@ -281,49 +277,49 @@ (fn [s] (if (contains? s path) (disj s path) - (conj s path))))))] + (conj s path)))))) - ;; Load library colors when the selected library (or filter options) change. - ;; - ;; flat current-colors* -- used for the grid view and the recent list view. - ;; grouped grouped-colors* -- used for the library grouped list view. - ;; - ;; Library colors are fully converted with `library-color->color` here so - ;; the render path never needs to do it. `flat-colors` is materialised as - ;; an eager vector so realisation does not leak into render time. - ;; open-groups* is reset to #{} (all groups expanded) on every library switch. - (mf/with-effect [selected recent-colors libraries file-id valid-color?] - (let [resolved-file-id (if (= selected :file) file-id selected)] - (reset! open-groups* #{}) - (if (= selected :recent) - (let [colors (into [] - (comp - (filter valid-color?) - (map-indexed (fn [index color] - (let [color (if (map? color) color {:color color})] - (vary-meta color assoc ::id (dm/str index))))) - (take-while some?)) - (sort ctc/sort-colors (reverse recent-colors)))] - (reset! current-colors* colors) - (reset! grouped-colors* {})) + resolved-file-id + (if (= selected :file) file-id selected) - (let [raw-colors (->> (dm/get-in libraries [resolved-file-id :data :colors]) - (vals) - (filter valid-color?) - (sort-by :name)) + ;; Colors of the selected library; stable across unrelated commits + library-colors + (when (not= selected :recent) + (dm/get-in libraries [resolved-file-id :data :colors])) - ;; Eager vector for the grid view -- index-based ::id for keying. - flat-colors (into [] - (map-indexed (fn [index color] - (-> (ctc/library-color->color color resolved-file-id) - (vary-meta assoc ::id (dm/str index))))) - raw-colors) + ;; Flat (grid and recent list) and grouped (library list) colors, converted for rendering + [current-colors grouped-colors] + (mf/with-memo [selected recent-colors library-colors resolved-file-id valid-color?] + (if (= selected :recent) + [(into [] + (comp + (filter valid-color?) + (map-indexed (fn [index color] + (let [color (if (map? color) color {:color color})] + (vary-meta color assoc ::id (dm/str index))))) + (take-while some?)) + (sort ctc/sort-colors (reverse recent-colors))) + {}] - ;; Group tree with colors already converted -- no conversions at render time. - grouped (some-> (grp/group-assets raw-colors false) - (convert-grouped-colors resolved-file-id))] - (reset! current-colors* flat-colors) - (reset! grouped-colors* (or grouped {})))))) + (let [raw-colors (->> (vals library-colors) + (filter valid-color?) + (sort-by :name)) + + ;; Eager vector for the grid view -- index-based ::id for keying. + flat-colors (into [] + (map-indexed (fn [index color] + (-> (ctc/library-color->color color resolved-file-id) + (vary-meta assoc ::id (dm/str index))))) + raw-colors) + + ;; Group tree with colors already converted -- no conversions at render time. + grouped (some-> (grp/group-assets raw-colors false) + (convert-grouped-colors resolved-file-id))] + [flat-colors (or grouped {})])))] + + ;; Expands all groups when the selected library changes + (mf/with-effect [selected] + (reset! open-groups* #{})) [:div {:class (stl/css :libraries)} [:div {:class (stl/css :select-wrapper)} diff --git a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs index 01804c409c..85617c307e 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/ramp.cljs @@ -121,10 +121,15 @@ (reset! internal-color* color) (on-change color))))] + ;; Syncs with color changes made outside the ramp; the colors the ramp + ;; emits come back with the same components and are skipped (mf/use-effect (mf/deps color) (fn [] - (reset! internal-color* (enrich-color-map color)))) + (let [color (enrich-color-map color)] + (when (not= (select-keys color [:h :s :v :alpha]) + (select-keys internal-color [:h :s :v :alpha])) + (reset! internal-color* color))))) [:* [:> value-saturation-selector* diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index f07dce3891..c1bcdb19a1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -98,6 +98,21 @@ (when (= (:type panel) :component-swap) [:> component-menu* {:shapes (:shapes panel) :is-swap-opened true}])) +(defn- get-shapes-with-children + "Returns the shapes of `selected` together with all their descendants." + [objects selected] + (loop [queue (into #queue [] selected) + visited selected] + (if-let [id (peek queue)] + (let [shape (get objects id) + children (:shapes shape)] + (if (seq children) + (let [new-children (remove visited children)] + (recur (into (pop queue) new-children) + (into visited new-children))) + (recur (pop queue) visited))) + (sequence (keep (d/getf objects)) visited)))) + (mf/defc design-menu* {::mf/private true} [{:keys [selected objects page-id file-id shapes]}] @@ -123,29 +138,16 @@ (->> (dm/get-in grid-edition [edition :selected]) (map #(dm/get-in objects [edition :layout-grid-cells %]))) - shapes-with-children* - (mf/use-state nil) + ;; Deferred, so the subtree walk runs in a background render + deferred-selected + (mf/use-deferred selected) - _ (mf/use-effect - (mf/deps selected objects shapes) - (fn [] - (reset! shapes-with-children* nil) - (let [result - (loop [queue (into #queue [] selected) - visited selected] - (if-let [id (peek queue)] - (let [shape (get objects id) - children (:shapes shape)] - (if (seq children) - (let [new-children (remove visited children)] - (recur (into (pop queue) new-children) - (into visited new-children))) - (recur (pop queue) visited))) - (sequence (keep (d/getf objects)) visited)))] - (reset! shapes-with-children* result)))) + deferred-objects + (mf/use-deferred objects) shapes-with-children - (deref shapes-with-children*) + (mf/with-memo [deferred-selected deferred-objects] + (get-shapes-with-children deferred-objects deferred-selected)) total-selected (count selected)] From 238a4d8613d4799b92eab0e38e5194fb72dc116d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 5 Oct 2026 11:56:59 +0200 Subject: [PATCH 002/145] :bug: Sanitize SVG namespace-prefixed elements and attributes (#12073) * :bug: Sanitize SVG namespace-prefixed elements and attributes The SVG sanitizer compared qualified tag and attribute names against its blocklists. The XML parser keeps the namespace prefix in the keyword name (:x:script), so a script element written with a prefix bound to the SVG namespace passed sanitization untouched and was later served as image/svg+xml, executing in the Penpot origin. Match tags and attributes on the local name (the part after the last colon) instead, so dangerous elements and attributes are removed no matter which prefix they use. Legitimately prefixed attributes such as xlink:href keep working. Closes #12071 AI-assisted-by: glm-5.3-flash * :recycle: Cover prefixed SVG vectors and hoist sanitizer blocklist Apply the code review findings on top of the sanitizer fix: - Add a test for the javascript: href vector using a prefix bound to the xlink namespace, the other payload variant the old exact-key check missed. - Add a test asserting that legitimately prefixed attributes such as xml:space are preserved by the local-name matching. - Hoist the dangerous-tags blocklist next to the other sanitizer policy defs and bind the keyword name once in local-name. Relates to #12071 AI-assisted-by: glm-5.3-flash --- backend/src/app/media/svg.clj | 57 +++++++++++++------- backend/test/backend_tests/media_test.clj | 66 +++++++++++++++++++++++ 2 files changed, 103 insertions(+), 20 deletions(-) diff --git a/backend/src/app/media/svg.clj b/backend/src/app/media/svg.clj index 1de52d4030..4cad562d91 100644 --- a/backend/src/app/media/svg.clj +++ b/backend/src/app/media/svg.clj @@ -52,30 +52,47 @@ (def ^:private dangerous-attrs-pattern #"(?i)^on\w+$") (def ^:private javascript-href-pattern #"(?i)^javascript:") +(def ^:private dangerous-tags + #{"script" "foreignObject" "set" + "animate" "animateTransform" "animateColor" "animateMotion"}) + +(defn- local-name + "Return the local name of a parsed tag or attribute keyword. + + The XML parser interns qualified names with the prefix included in + the keyword name and no keyword namespace: `` parses as + the keyword `:x:script`. Dangerous elements and attributes behave + the same in the browser regardless of the prefix used, so matching + is done on the local name: the part after the last `:`." + [k] + (let [n (name k)] + (if (str/includes? n ":") + (last (str/split n ":")) + n))) + (defn- sanitize-svg-element "Recursively sanitize an SVG element by removing dangerous tags and attributes." [{:keys [tag attrs content] :as element}] (when (and (map? element) tag) - (let [dangerous-tags #{:script :foreignObject :set :animate :animateTransform :animateColor :animateMotion}] - (when-not (contains? dangerous-tags tag) - (let [clean-attrs (->> attrs - (remove (fn [[k v]] - (or (re-matches dangerous-attrs-pattern (name k)) - (and (#{:href :xlink:href} k) - (string? v) - (re-find javascript-href-pattern (str/trim v)))))) - (into {})) - clean-content (when content - (->> content - (filter #(or (string? %) (map? %))) - (map (fn [child] - (if (map? child) - (sanitize-svg-element child) - child))) - (filter some?) - vec))] - (cond-> {:tag tag :attrs clean-attrs} - (seq clean-content) (assoc :content clean-content))))))) + (when-not (contains? dangerous-tags (local-name tag)) + (let [clean-attrs (->> attrs + (remove (fn [[k v]] + (or (re-matches dangerous-attrs-pattern (local-name k)) + (and (= "href" (local-name k)) + (string? v) + (re-find javascript-href-pattern (str/trim v)))))) + (into {})) + clean-content (when content + (->> content + (filter #(or (string? %) (map? %))) + (map (fn [child] + (if (map? child) + (sanitize-svg-element child) + child))) + (filter some?) + vec))] + (cond-> {:tag tag :attrs clean-attrs} + (seq clean-content) (assoc :content clean-content)))))) (defn sanitize-svg "Sanitize SVG content by removing dangerous elements and attributes. diff --git a/backend/test/backend_tests/media_test.clj b/backend/test/backend_tests/media_test.clj index eb2f2517c7..3c3a887032 100644 --- a/backend/test/backend_tests/media_test.clj +++ b/backend/test/backend_tests/media_test.clj @@ -137,6 +137,72 @@ (t/is (not (clojure.string/includes? result "onmouseover"))) (t/is (clojure.string/includes? result "alert('xss')" + result (svg/sanitize-svg svg)] + (t/is (not (clojure.string/includes? result "alert('xss')" + result (svg/sanitize-svg svg)] + (t/is (not (clojure.string/includes? result "alert('xss')" + result (svg/sanitize-svg svg)] + (t/is (not (clojure.string/includes? result "" + result (svg/sanitize-svg svg)] + (t/is (not (clojure.string/includes? result "foreignObject"))) + (t/is (not (clojure.string/includes? result "" + result (svg/sanitize-svg svg)] + (t/is (not (clojure.string/includes? result "javascript:"))) + (t/is (not (clojure.string/includes? result "alert"))) + (t/is (clojure.string/includes? result "" + result (svg/sanitize-svg svg)] + (t/is (not (clojure.string/includes? result "onload"))) + (t/is (not (clojure.string/includes? result "onclick"))) + (t/is (not (clojure.string/includes? result "alert"))) + (t/is (clojure.string/includes? result "" + result (svg/sanitize-svg svg)] + (t/is (not (clojure.string/includes? result "javascript:"))) + (t/is (not (clojure.string/includes? result "alert"))) + (t/is (clojure.string/includes? result "hola" + result (svg/sanitize-svg svg)] + (t/is (clojure.string/includes? result "xml:space")) + (t/is (clojure.string/includes? result "hola"))))) + (t/deftest info-invalid-image (t/testing "info on invalid image raises error" (let [path (fs/create-tempfile :prefix "penpot-test-" :suffix ".jpg")] From 331e9168b58e322ceea821d4ae5a1b803fbbfe4e Mon Sep 17 00:00:00 2001 From: Shreyash Agare Date: Mon, 5 Oct 2026 15:34:11 +0530 Subject: [PATCH 003/145] :bug: Keep interactions of flex children when syncing components (#11788) * :bug: Keep interactions of flex children when syncing components The flex-child sync between main and copy children propagated every attribute in swap-keep-attrs, which includes :interactions. Since :interactions has no sync group, the touched check never protected it, so any change that reflowed a flex component (like toggling a child's visibility) overwrote the copy children's interactions with the main's. Closes #11746 AI-assisted-by: claude-opus-5 Signed-off-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com> * :bug: Add another test and simplify comments --------- Signed-off-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com> Co-authored-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com> Co-authored-by: alonso.torres --- common/src/app/common/logic/libraries.cljc | 8 +- .../logic/comp_flex_interactions_test.cljc | 155 ++++++++++++++++++ common/test/common_tests/runner.cljc | 2 + 3 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 common/test/common_tests/logic/comp_flex_interactions_test.cljc diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index fc3c4d00de..5d003a64b2 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -56,6 +56,10 @@ ;; We don't do automatic update of the `layout-grid-cells` property. (remove #(= :layout-grid-cells %)))) +;; Flex-child layout attrs synced between main and copy (interactions are only kept on swap) +(def ^:private flex-child-attrs + (disj ctk/swap-keep-attrs :interactions)) + (defn enabled-shape? [id container] (or (empty? log-shape-ids) @@ -2402,7 +2406,7 @@ (fn [child-copy] (let [child-main (ctf/get-ref-shape main-container main-component child-copy)] (-> child-copy - (propagate-attrs child-main ctk/swap-keep-attrs omit-touched?)))) + (propagate-attrs child-main flex-child-attrs omit-touched?)))) {:ignore-touched true}))] (pcb/concat-changes changes new-changes))) @@ -2432,7 +2436,7 @@ (fn [child-main] (let [child-copy (ctf/get-shape-in-copy copy-container child-main shape-copy)] (-> child-main - (propagate-attrs child-copy ctk/swap-keep-attrs omit-touched?)))) + (propagate-attrs child-copy flex-child-attrs omit-touched?)))) {:ignore-touched true}))] (pcb/concat-changes changes new-changes))) diff --git a/common/test/common_tests/logic/comp_flex_interactions_test.cljc b/common/test/common_tests/logic/comp_flex_interactions_test.cljc new file mode 100644 index 0000000000..5ded09e9a3 --- /dev/null +++ b/common/test/common_tests/logic/comp_flex_interactions_test.cljc @@ -0,0 +1,155 @@ +;; 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 SUBSIDIARY SL + +(ns common-tests.logic.comp-flex-interactions-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.math :as mth] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.container :as ctn] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(defn- setup-file + "Flex component and copy whose children navigate to different boards." + [] + (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :main-root-params {:layout :flex + :layout-flex-dir :row} + :copy-root-params {:children-labels [:copy-child]}) + (ths/add-sample-shape :main-popup :type :frame) + (ths/add-sample-shape :copy-popup :type :frame) + (ths/add-interaction :main-child :main-popup) + (ths/add-interaction :copy-child :copy-popup))) + +(defn- update-shape + [file label update-fn] + (let [page (thf/current-page file)] + (thf/apply-changes + file + (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(thi/id label)} + update-fn + (:objects page) + {})))) + +(defn- destinations + [shape] + (mapv :destination (:interactions shape))) + +(t/deftest test-sync-flex-main-keeps-copy-interactions + (let [;; ==== Setup + file (setup-file) + margin {:m1 5 :m2 5 :m3 5 :m4 5} + + ;; ==== Action + updated-file (update-shape file :main-child + #(assoc % :hidden true :layout-item-margin margin)) + changes (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + file' (thf/apply-changes updated-file changes) + + ;; ==== Get + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (true? (:hidden copy-child'))) + (t/is (= margin (:layout-item-margin copy-child'))) + (t/is (= [(thi/id :copy-popup)] (destinations copy-child'))))) + +(t/deftest test-sync-flex-copy-to-main-keeps-main-interactions + (let [;; ==== Setup + file (setup-file) + + ;; ==== Action + updated-file (update-shape file :copy-child #(assoc % :hidden true)) + page (thf/current-page updated-file) + container (ctn/make-container page :page) + changes (-> (pcb/empty-changes) + (pcb/with-container container) + (cll/generate-sync-shape-inverse (:data updated-file) + {(:id updated-file) updated-file} + container + (thi/id :copy-root))) + file' (thf/apply-changes updated-file changes) + + ;; ==== Get + main-child' (ths/get-shape file' :main-child)] + + ;; ==== Check + (t/is (true? (:hidden main-child'))) + (t/is (= [(thi/id :main-popup)] (destinations main-child'))))) + +(defn- setup-library-and-file + "Library flex component with a nested copy, and a file instance whose + nested copy has an interaction." + [] + (let [library (-> (thf/sample-file :library) + (tho/add-nested-component :component1 + :main1-root + :main1-child + :component2 + :main2-root + :nested-head + :main2-root-params {:layout :flex + :layout-flex-dir :row})) + file (-> (thf/sample-file :file) + (ths/add-sample-shape :copy-popup :type :frame) + (thc/instantiate-component :component2 + :copy2-root + :library library + :children-labels [:copy-nested-head]) + (ths/add-interaction :copy-nested-head :copy-popup))] + [library file])) + +(defn- sync-file-from-library + [file library] + (cll/generate-sync-file-changes (pcb/empty-changes) + nil + nil + (:id file) + nil + (:id library) + {(:id library) library + (:id file) file} + (:id file))) + +(t/deftest test-sync-library-flex-keeps-nested-copy-interactions + (let [;; ==== Setup + [library file] (setup-library-and-file) + copy-x (:x (tho/bottom-shape file :copy2-root)) + + ;; ==== Action + library' (-> library + (update-shape :main1-child #(gsh/move % (gpt/point 1 0))) + (tho/propagate-component-changes :component1)) + file' (thf/apply-changes file (sync-file-from-library file library')) + + ;; ==== Get + copy-nested-head' (ths/get-shape file' :copy-nested-head)] + + ;; ==== Check + (t/is (mth/close? (inc copy-x) (:x (tho/bottom-shape file' :copy2-root)))) + (t/is (= [(thi/id :copy-popup)] (destinations copy-nested-head'))))) diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 081516c73c..eb2e3bb1c7 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -56,6 +56,7 @@ [common-tests.logic.chained-propagation-test] [common-tests.logic.comp-creation-test] [common-tests.logic.comp-detach-with-nested-test] + [common-tests.logic.comp-flex-interactions-test] [common-tests.logic.comp-main-edit-breaks-copy-slots-test] [common-tests.logic.comp-remove-swap-slots-test] [common-tests.logic.comp-reset-test] @@ -157,6 +158,7 @@ 'common-tests.logic.chained-propagation-test 'common-tests.logic.comp-creation-test 'common-tests.logic.comp-detach-with-nested-test + 'common-tests.logic.comp-flex-interactions-test 'common-tests.logic.comp-main-edit-breaks-copy-slots-test 'common-tests.logic.comp-remove-swap-slots-test 'common-tests.logic.comp-reset-test From 4a4f50e7ea4cb453cda48d7a96e907209ab1f63c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 5 Oct 2026 10:15:16 +0000 Subject: [PATCH 004/145] :fire: Stop printing separator lines in reports Drop the equals-sign banners from the CLJS throwable formatter, so new frontend error reports are born without them. The JVM branch never printed them. Adds a test asserting full reports carry sections but no separator runs. AI-assisted-by: Muse Spark 1.3 Free --- common/src/app/common/exceptions.cljc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index 86785c58ff..588391601a 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -245,7 +245,6 @@ (defn format-throwable [cause & {:as opts}] (with-out-str - (println "====================") (when-let [exdata (ex-data cause)] (when-let [hint (or (get exdata :hint) (ex-message cause))] @@ -275,9 +274,7 @@ (when-let [trace (.-stack cause)] (println "Trace:") (println "--------------------") - (println (.-stack cause))) - - (println "====================")))) + (println (.-stack cause)))))) (defn first-line [s] From cf46b53bcb61c2a9be8f202bec03647a5470d43d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 5 Oct 2026 12:18:37 +0200 Subject: [PATCH 005/145] :books: Update changelog --- CHANGES.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 720cbc0fb9..56d1046320 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # CHANGELOG +## 2.18.2 + +### :bug: Bugs fixed + +- Fix workspace showing an internal error when syncing components or dragging in the color picker [#11933](https://github.com/penpot/penpot/issues/11933) (PR: [#11941](https://github.com/penpot/penpot/pull/11941)) +- Fix SVG sanitizer keeping namespace-prefixed script elements when uploading SVG files [#12071](https://github.com/penpot/penpot/issues/12071) (PR: [#12073](https://github.com/penpot/penpot/pull/12073)) + ## 2.18.1 ### :bug: Bugs fixed From a3ba4702d76bb2b9ee9d87f711d0d6dcb56864ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:02:23 +0000 Subject: [PATCH 006/145] :arrow_up: Bump the github-actions group across 1 directory with 8 updates Bumps the github-actions group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/create-github-app-token](https://github.com/actions/create-github-app-token) | `1.12.0` | `3.2.0` | | [actions/github-script](https://github.com/actions/github-script) | `7.1.0` | `9.0.0` | | [actions/checkout](https://github.com/actions/checkout) | `6.1.0` | `7.0.1` | | [pnpm/setup](https://github.com/pnpm/setup) | `2.0.2` | `3.0.0` | | [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) | `4.3.1` | `6.3.0` | | [actions/cache/restore](https://github.com/actions/cache) | `5.1.0` | `6.1.0` | | [actions/cache/save](https://github.com/actions/cache) | `5.1.0` | `6.1.0` | | [actions/download-artifact](https://github.com/actions/download-artifact) | `7.0.0` | `8.0.1` | Updates `actions/create-github-app-token` from 1.12.0 to 3.2.0 - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/create-github-app-token/compare/d72941d797fd3113feb6b93fd0dec494b13a2547...bcd2ba49218906704ab6c1aa796996da409d3eb1) Updates `actions/github-script` from 7.1.0 to 9.0.0 - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/f28e40c7f34bde8b3046d885e986cb6290c5673b...3a2844b7e9c422d3c10d287c895573f7108da1b3) Updates `actions/checkout` from 6.1.0 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/d23441a48e516b6c34aea4fa41551a30e30af803...3d3c42e5aac5ba805825da76410c181273ba90b1) Updates `pnpm/setup` from 2.0.2 to 3.0.0 - [Release notes](https://github.com/pnpm/setup/releases) - [Commits](https://github.com/pnpm/setup/compare/84cb39b217b10273981911c288cd62326dc7c6d2...fbda4c85fc2e1e08721cd8763afea8f48d60f024) Updates `aws-actions/configure-aws-credentials` from 4.3.1 to 6.3.0 - [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases) - [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/7474bc4690e29a8392af63c5b98e7449536d5c3a...e1253824e5c10ff9df46874f81ed3ec929e19cfd) Updates `actions/cache/restore` from 5.1.0 to 6.1.0 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/caa296126883cff596d87d8935842f9db880ef25...55cc8345863c7cc4c66a329aec7e433d2d1c52a9) Updates `actions/cache/save` from 5.1.0 to 6.1.0 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/caa296126883cff596d87d8935842f9db880ef25...55cc8345863c7cc4c66a329aec7e433d2d1c52a9) Updates `actions/download-artifact` from 7.0.0 to 8.0.1 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/37930b1c2abaa49bbe596cd826c3c89aef350131...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 3.2.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/github-script dependency-version: 9.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: pnpm/setup dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: aws-actions/configure-aws-credentials dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/cache/restore dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/cache/save dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/download-artifact dependency-version: 8.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/auto-label.yml | 4 ++-- .github/workflows/build-bundle.yml | 4 ++-- .github/workflows/build-docker-devenv.yml | 2 +- .github/workflows/build-docker.yml | 4 ++-- .github/workflows/lint-workflows.yml | 2 +- .github/workflows/plugins-deploy-api-doc.yml | 4 ++-- .github/workflows/plugins-deploy-package.yml | 2 +- .github/workflows/plugins-deploy-packages.yml | 2 +- .../workflows/plugins-deploy-styles-doc.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/tests-backend.yml | 2 +- .github/workflows/tests-common.yml | 2 +- .github/workflows/tests-e2e.yml | 22 +++++++++---------- .github/workflows/tests-exporter.yml | 2 +- .github/workflows/tests-frontend.yml | 2 +- .github/workflows/tests-library.yml | 2 +- .github/workflows/tests-mcp.yml | 2 +- .github/workflows/tests-plugins.yml | 2 +- .github/workflows/tests-wasm.yml | 2 +- 19 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index 0b6838f7c8..b64aa8e4ba 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Generate GitHub App token id: triage-app-token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ secrets.TRIAGE_APP_ID }} private-key: ${{ secrets.TRIAGE_APP_PRIVATE_KEY }} @@ -29,7 +29,7 @@ jobs: permission-organization-projects: write - name: Process Issue or PR - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ steps.triage-app-token.outputs.token }} script: | diff --git a/.github/workflows/build-bundle.yml b/.github/workflows/build-bundle.yml index 1172790408..88c04a2634 100644 --- a/.github/workflows/build-bundle.yml +++ b/.github/workflows/build-bundle.yml @@ -68,7 +68,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 @@ -136,7 +136,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/build-docker-devenv.yml b/.github/workflows/build-docker-devenv.yml index 87856225bb..906adcd369 100644 --- a/.github/workflows/build-docker-devenv.yml +++ b/.github/workflows/build-docker-devenv.yml @@ -18,7 +18,7 @@ jobs: echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV - name: Checkout code - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index a794565d25..a8d6a1976d 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -90,7 +90,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 @@ -188,7 +188,7 @@ jobs: echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}-${{ matrix.image }}" >> $GITHUB_ENV - name: Checkout code - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false ref: ${{ inputs.sha != '' && inputs.sha || inputs.gh_ref }} diff --git a/.github/workflows/lint-workflows.yml b/.github/workflows/lint-workflows.yml index 23f9849899..eb3ca73ed3 100644 --- a/.github/workflows/lint-workflows.yml +++ b/.github/workflows/lint-workflows.yml @@ -48,7 +48,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/plugins-deploy-api-doc.yml b/.github/workflows/plugins-deploy-api-doc.yml index 4d0e5754d2..3bd9470178 100644 --- a/.github/workflows/plugins-deploy-api-doc.yml +++ b/.github/workflows/plugins-deploy-api-doc.yml @@ -39,7 +39,7 @@ jobs: echo "gh_ref=$GH_REF" >> $GITHUB_OUTPUT - name: Checkout - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 @@ -48,7 +48,7 @@ jobs: # No dependency cache here: a deploy must not restore a cache that # other workflows can write to. - name: Setup pnpm + Node.js - uses: pnpm/setup@84cb39b217b10273981911c288cd62326dc7c6d2 # v2.0.2 + uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0 with: working-directory: plugins runtime: node@24.21.0 diff --git a/.github/workflows/plugins-deploy-package.yml b/.github/workflows/plugins-deploy-package.yml index 6165dd465b..c9ff731682 100644 --- a/.github/workflows/plugins-deploy-package.yml +++ b/.github/workflows/plugins-deploy-package.yml @@ -65,7 +65,7 @@ jobs: fi - name: Checkout - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/plugins-deploy-packages.yml b/.github/workflows/plugins-deploy-packages.yml index 11650fd5ef..40b71ede21 100644 --- a/.github/workflows/plugins-deploy-packages.yml +++ b/.github/workflows/plugins-deploy-packages.yml @@ -39,7 +39,7 @@ jobs: # [For new plugins] # Add more outputs here steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - id: filter diff --git a/.github/workflows/plugins-deploy-styles-doc.yml b/.github/workflows/plugins-deploy-styles-doc.yml index 0f04b62615..0cba3f685d 100644 --- a/.github/workflows/plugins-deploy-styles-doc.yml +++ b/.github/workflows/plugins-deploy-styles-doc.yml @@ -37,7 +37,7 @@ jobs: echo "gh_ref=$GH_REF" >> $GITHUB_OUTPUT - name: Checkout - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 @@ -46,7 +46,7 @@ jobs: # No dependency cache here: a deploy must not restore a cache that # other workflows can write to. - name: Setup pnpm + Node.js - uses: pnpm/setup@84cb39b217b10273981911c288cd62326dc7c6d2 # v2.0.2 + uses: pnpm/setup@fbda4c85fc2e1e08721cd8763afea8f48d60f024 # v3.0.0 with: working-directory: plugins runtime: node@24.21.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e956f2310d..201150b826 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: echo "gh_ref=$GH_REF" >> $GITHUB_OUTPUT - name: Checkout code - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 @@ -55,7 +55,7 @@ jobs: # --- Publicly release the docker images --- - name: Configure ECR credentials - uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 + uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 with: aws-access-key-id: ${{ secrets.DOCKER_USERNAME }} aws-secret-access-key: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/tests-backend.yml b/.github/workflows/tests-backend.yml index 6a447d5737..a1df06c91a 100644 --- a/.github/workflows/tests-backend.yml +++ b/.github/workflows/tests-backend.yml @@ -63,7 +63,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/tests-common.yml b/.github/workflows/tests-common.yml index a4d32c39b1..f8168dbade 100644 --- a/.github/workflows/tests-common.yml +++ b/.github/workflows/tests-common.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/tests-e2e.yml b/.github/workflows/tests-e2e.yml index 0f6f0f8765..84db8d144d 100644 --- a/.github/workflows/tests-e2e.yml +++ b/.github/workflows/tests-e2e.yml @@ -110,7 +110,7 @@ jobs: # An empty `ref` makes checkout fall back to its default (the PR merge # ref on pull_request, the pushed ref on push). - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false ref: ${{ inputs.gh_ref }} @@ -124,7 +124,7 @@ jobs: - name: Restore Bundle Cache id: restore - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: ${{ steps.vars.outputs.bundle_key }} path: frontend/resources/public @@ -137,7 +137,7 @@ jobs: - name: Store Bundle Cache if: steps.restore.outputs.cache-hit != 'true' - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: ${{ steps.vars.outputs.bundle_key }} path: frontend/resources/public @@ -167,13 +167,13 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false ref: ${{ inputs.gh_ref }} - name: Restore Cache - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: ${{ needs.build-bundle.outputs.bundle_key }} path: frontend/resources/public @@ -239,13 +239,13 @@ jobs: - /var/cache/github-runner/gitlib:/root/.gitlibs steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # The driver serves the prebuilt bundle from frontend/resources/public. - name: Restore Cache - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: ${{ needs.build-bundle.outputs.bundle_key }} path: frontend/resources/public @@ -279,13 +279,13 @@ jobs: - /var/cache/github-runner/gitlib:/root/.gitlibs steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # Mocked mode serves the prebuilt bundle from frontend/resources/public. - name: Restore Cache - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: ${{ needs.build-bundle.outputs.bundle_key }} path: frontend/resources/public @@ -327,7 +327,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false ref: ${{ inputs.gh_ref }} @@ -338,7 +338,7 @@ jobs: pnpm install --frozen-lockfile; - name: Download blob reports - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: frontend/all-blob-reports pattern: integration-blob-report-* diff --git a/.github/workflows/tests-exporter.yml b/.github/workflows/tests-exporter.yml index db32e3f59c..82b7b12f73 100644 --- a/.github/workflows/tests-exporter.yml +++ b/.github/workflows/tests-exporter.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/tests-frontend.yml b/.github/workflows/tests-frontend.yml index eff25d11df..9a6ff9ae4b 100644 --- a/.github/workflows/tests-frontend.yml +++ b/.github/workflows/tests-frontend.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/tests-library.yml b/.github/workflows/tests-library.yml index 5a714705fb..76dbc0d079 100644 --- a/.github/workflows/tests-library.yml +++ b/.github/workflows/tests-library.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/tests-mcp.yml b/.github/workflows/tests-mcp.yml index 50dc8a6806..8ccf4a083a 100644 --- a/.github/workflows/tests-mcp.yml +++ b/.github/workflows/tests-mcp.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/tests-plugins.yml b/.github/workflows/tests-plugins.yml index 01ed31eccf..fff9c8002e 100644 --- a/.github/workflows/tests-plugins.yml +++ b/.github/workflows/tests-plugins.yml @@ -41,7 +41,7 @@ jobs: - /var/cache/github-runner/gitlib:/root/.gitlibs steps: - - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/tests-wasm.yml b/.github/workflows/tests-wasm.yml index 135d65a045..f3e1547321 100644 --- a/.github/workflows/tests-wasm.yml +++ b/.github/workflows/tests-wasm.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false From 6435261359dbd397fa2338ba5eca20751c1b0984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 5 Oct 2026 13:11:08 +0200 Subject: [PATCH 007/145] :wrench: Use client-id instead of app-id in auto-label workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/create-github-app-token v3 deprecates the app-id input in favour of client-id. Switch to the new TRIAGE_APP_CLIENT_ID secret. Signed-off-by: David Barragán Merino --- .github/workflows/auto-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index b64aa8e4ba..7467e61a79 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -21,7 +21,7 @@ jobs: id: triage-app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: - app-id: ${{ secrets.TRIAGE_APP_ID }} + client-id: ${{ secrets.TRIAGE_APP_CLIENT_ID }} private-key: ${{ secrets.TRIAGE_APP_PRIVATE_KEY }} # No owner/repositories: the token is scoped to this repository. # Organization permissions (the project board) still apply. From 2c4da981313ba4a6711fa5883cf922ed5c9d46a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 5 Oct 2026 14:32:04 +0200 Subject: [PATCH 008/145] :bug: Request pull-requests permission in auto-label token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Barragán Merino --- .github/workflows/auto-label.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index 7467e61a79..e3c435e40d 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -26,6 +26,7 @@ jobs: # No owner/repositories: the token is scoped to this repository. # Organization permissions (the project board) still apply. permission-issues: write + permission-pull-requests: write permission-organization-projects: write - name: Process Issue or PR From ab58922f2c38270bb6c2120940be684b8f35f50e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 5 Oct 2026 16:47:07 +0200 Subject: [PATCH 009/145] :ambulance: Revoke file access when a member leaves the team (#12097) Delete the team-scoped file, project and pin relations on leave-team and delete-team-member, require a live team membership in the file, project, search and listing permission queries, and clean pre-existing orphan rows with a migration. Former members now get not-found on file actions instead of keeping read and rename access (GHSA-v9r9-h77c-55m2). AI-assisted-by: muse-spark-1.3-contributor --- .../auth-permissions-product-domains.md | 1 + backend/src/app/binfile/common.clj | 12 +- backend/src/app/migrations.clj | 5 +- ...l-orphan-profile-rels-after-team-leave.sql | 27 ++ backend/src/app/rpc/commands/projects.clj | 12 +- backend/src/app/rpc/commands/search.clj | 4 + backend/src/app/rpc/commands/teams.clj | 38 ++- backend/test/backend_tests/helpers.clj | 4 +- backend/test/backend_tests/rpc_team_test.clj | 312 ++++++++++++++++++ 9 files changed, 404 insertions(+), 11 deletions(-) create mode 100644 backend/src/app/migrations/sql/0153-del-orphan-profile-rels-after-team-leave.sql diff --git a/.serena/memories/backend/auth-permissions-product-domains.md b/.serena/memories/backend/auth-permissions-product-domains.md index c3057118d5..130cd62609 100644 --- a/.serena/memories/backend/auth-permissions-product-domains.md +++ b/.serena/memories/backend/auth-permissions-product-domains.md @@ -15,6 +15,7 @@ - `app.rpc.permissions` provides predicate/check factories. Failed permission checks intentionally raise `:not-found` / `:object-not-found`, not an authorization-specific error, to avoid leaking object existence. - Team role flags are normalized as owner > admin > editor > viewer. Owner/admin imply edit; any membership row implies read. +- File/project role rows only count with a live team membership: the file/project permission queries require a matching `team_profile_rel` row, and leaving or removing a member deletes their team-scoped `file`/`project`/`team-project-profile-rel` rows (GHSA-v9r9-h77c-55m2). - File/project/comment checks are implemented in the owning command namespaces, often via helpers imported from `files`, `teams`, or `projects`; do not bypass those helpers with direct DB lookups unless preserving their not-found semantics. - Comment permission includes both logged-in state and the file/team comment policy. Shared viewer paths may pass `share-id`; preserve that path when changing comment queries. diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index 3e4402be92..e2aed9a837 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -344,8 +344,13 @@ fpr.can_edit from file_profile_rel as fpr inner join file as f on (f.id = fpr.file_id) + inner join project as p on (p.id = f.project_id) where fpr.file_id = ? and fpr.profile_id = ? + and exists (select 1 + from team_profile_rel as tpr + where tpr.team_id = p.team_id + and tpr.profile_id = fpr.profile_id) union all select tpr.is_owner, tpr.is_admin, @@ -361,8 +366,13 @@ ppr.can_edit from project_profile_rel as ppr inner join file as f on (f.project_id = ppr.project_id) + inner join project as p on (p.id = ppr.project_id) where f.id = ? - and ppr.profile_id = ?") + and ppr.profile_id = ? + and exists (select 1 + from team_profile_rel as tpr + where tpr.team_id = p.team_id + and tpr.profile_id = ppr.profile_id)") (defn- get-file-permissions* [conn profile-id file-id] diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 2edb8614d5..13f99170fe 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -499,7 +499,10 @@ :fn (mg/resource "app/migrations/sql/0152-improve-uuid-defaults-and-drop-extension.sql")} {:name "0152-rename-version-and-add-indexes-to-server-error-report" - :fn (mg/resource "app/migrations/sql/0152-rename-version-and-add-indexes-to-server-error-report.sql")}]) + :fn (mg/resource "app/migrations/sql/0152-rename-version-and-add-indexes-to-server-error-report.sql")} + + {:name "0153-del-orphan-profile-rels-after-team-leave" + :fn (mg/resource "app/migrations/sql/0153-del-orphan-profile-rels-after-team-leave.sql")}]) (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0153-del-orphan-profile-rels-after-team-leave.sql b/backend/src/app/migrations/sql/0153-del-orphan-profile-rels-after-team-leave.sql new file mode 100644 index 0000000000..41f30d7796 --- /dev/null +++ b/backend/src/app/migrations/sql/0153-del-orphan-profile-rels-after-team-leave.sql @@ -0,0 +1,27 @@ +-- Remove team-scoped role rows whose profile is no longer a member of +-- the team that owns the project/file. Leftover from before +-- leave-team/delete-team-member cleaned them up (GHSA-v9r9-h77c-55m2). +-- Rows with a live team membership are kept. + +DELETE FROM file_profile_rel AS fpr +USING file AS f +INNER JOIN project AS p ON (p.id = f.project_id) +WHERE fpr.file_id = f.id + AND NOT EXISTS (SELECT 1 + FROM team_profile_rel AS tpr + WHERE tpr.team_id = p.team_id + AND tpr.profile_id = fpr.profile_id); + +DELETE FROM project_profile_rel AS ppr +USING project AS p +WHERE ppr.project_id = p.id + AND NOT EXISTS (SELECT 1 + FROM team_profile_rel AS tpr + WHERE tpr.team_id = p.team_id + AND tpr.profile_id = ppr.profile_id); + +DELETE FROM team_project_profile_rel AS tppr +WHERE NOT EXISTS (SELECT 1 + FROM team_profile_rel AS tpr + WHERE tpr.team_id = tppr.team_id + AND tpr.profile_id = tppr.profile_id); diff --git a/backend/src/app/rpc/commands/projects.clj b/backend/src/app/rpc/commands/projects.clj index 05d5ae79aa..ab678db840 100644 --- a/backend/src/app/rpc/commands/projects.clj +++ b/backend/src/app/rpc/commands/projects.clj @@ -41,8 +41,13 @@ ppr.is_admin, ppr.can_edit from project_profile_rel as ppr + inner join project as p on (p.id = ppr.project_id) where ppr.project_id = ? - and ppr.profile_id = ?") + and ppr.profile_id = ? + and exists (select 1 + from team_profile_rel as tpr + where tpr.team_id = p.team_id + and tpr.profile_id = ppr.profile_id)") (defn- get-permissions [conn profile-id project-id] @@ -143,7 +148,12 @@ inner join team as t on (t.id = p2.team_id) where p2.id in (select project_id from project_profile_rel as ppr + inner join project as p on (p.id = ppr.project_id) where ppr.profile_id = ? + and exists (select 1 + from team_profile_rel as tpr + where tpr.team_id = p.team_id + and tpr.profile_id = ppr.profile_id) and (ppr.can_edit = true or ppr.is_owner = true or ppr.is_admin = true)) diff --git a/backend/src/app/rpc/commands/search.clj b/backend/src/app/rpc/commands/search.clj index 7b60e6db30..463bfb4abd 100644 --- a/backend/src/app/rpc/commands/search.clj +++ b/backend/src/app/rpc/commands/search.clj @@ -32,6 +32,10 @@ where ppr.profile_id = ? and p.team_id = ? and (p.deleted_at is null) + and exists (select 1 + from team_profile_rel as tpr + where tpr.team_id = p.team_id + and tpr.profile_id = ppr.profile_id) and (ppr.is_admin = true or ppr.is_owner = true or ppr.can_edit = true) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 9c13d6b37d..1a329773f2 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -732,6 +732,37 @@ ;; --- Mutation: Leave Team +;; Remove every team-scoped relation of a profile that leaves (or is +;; removed from) a team. File and project roles hang from the team +;; membership: keeping them would let the former member keep reading +;; and editing files through `sql:file-permissions`. Pins are personal +;; state of the same team, so they go too. Other teams are untouched, +;; and rejoining grants fresh roles. Runs inside the caller's +;; transaction. +(def ^:private sql:delete-team-file-rels + "DELETE FROM file_profile_rel AS fpr + USING file AS f + INNER JOIN project AS p ON (p.id = f.project_id) + WHERE fpr.file_id = f.id + AND p.team_id = ? + AND fpr.profile_id = ?") + +(def ^:private sql:delete-team-project-rels + "DELETE FROM project_profile_rel AS ppr + USING project AS p + WHERE ppr.project_id = p.id + AND p.team_id = ? + AND ppr.profile_id = ?") + +(defn- delete-team-rels + [conn profile-id team-id] + (db/exec! conn [sql:delete-team-file-rels team-id profile-id]) + (db/exec! conn [sql:delete-team-project-rels team-id profile-id]) + (db/delete! conn :team-project-profile-rel {:profile-id profile-id + :team-id team-id}) + (db/delete! conn :team-profile-rel {:profile-id profile-id + :team-id team-id})) + (defn leave-team [{:keys [::db/conn ::mbus/msgbus]} {:keys [profile-id id reassign-to]}] (let [perms (get-permissions conn profile-id id) @@ -782,9 +813,7 @@ :code :owner-cant-leave-team :hint "releasing owner before leave")) - (db/delete! conn :team-profile-rel - {:profile-id profile-id - :team-id id}) + (delete-team-rels conn profile-id id) nil)) @@ -966,8 +995,7 @@ (ex/raise :type :validation :code :cant-remove-owner)) - (db/delete! conn :team-profile-rel {:profile-id member-id - :team-id team-id}) + (delete-team-rels conn member-id team-id) ;; A removed member that owns the organization of this team keeps ;; read-only access to it, so instead of kicking them out we degrade diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 25aa8b20bc..a9dd895176 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -299,9 +299,7 @@ ([params] (create-project-role* *system* params)) ([system {:keys [project-id profile-id role] :or {role :owner}}] (dm/with-open [conn (db/open system)] - (#'teams/create-project-role conn {:project-id project-id - :profile-id profile-id - :role role})))) + (#'teams/create-project-role conn profile-id project-id role)))) (defn create-file-role* ([params] (create-file-role* *system* params)) diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index 1c30415db2..0665c92570 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -1471,3 +1471,315 @@ (t/is (not (th/success? out))) (t/is (th/ex-of-type? (:error out) :not-found)) (t/is (th/ex-of-code? (:error out) :member-does-not-exist))))) + +(t/deftest leaver-loses-file-access + ;; GHSA-v9r9-h77c-55m2: leaving the team must revoke the file and + ;; project roles granted in that team. + (let [owner (th/create-profile* 1 {:is-active true}) + editor (th/create-profile* 2 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id owner)})] + + (th/create-team-role* {:team-id (:id team) + :profile-id (:id editor) + :role :editor}) + + (let [project (th/create-project* 1 {:profile-id (:id editor) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id editor) + :project-id (:id project)})] + + (t/testing "editor can rename and read summary before leaving" + (let [out (th/command! {::th/type :rename-file + ::rpc/profile-id (:id editor) + :id (:id file) + :name "renamed"})] + (t/is (th/success? out))) + (let [out (th/command! {::th/type :get-file-summary + ::rpc/profile-id (:id editor) + :id (:id file)})] + (t/is (th/success? out)))) + + (t/testing "leave team" + (let [out (th/command! {::th/type :leave-team + ::rpc/profile-id (:id editor) + :id (:id team)})] + (t/is (th/success? out)))) + + (t/testing "renaming after leaving fails with not-found" + (let [out (th/command! {::th/type :rename-file + ::rpc/profile-id (:id editor) + :id (:id file) + :name "renamed-again"}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))) + + (t/testing "file summary after leaving fails with not-found" + (let [out (th/command! {::th/type :get-file-summary + ::rpc/profile-id (:id editor) + :id (:id file)}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))) + + (t/testing "get-file after leaving fails with not-found" + (let [out (th/command! {::th/type :get-file + ::rpc/profile-id (:id editor) + :id (:id file) + :components-v2 true}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found))))))) + +(t/deftest removed-member-loses-file-access + ;; Same as above through the expulsion path. + (let [owner (th/create-profile* 1 {:is-active true}) + editor (th/create-profile* 2 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id owner)})] + + (th/create-team-role* {:team-id (:id team) + :profile-id (:id editor) + :role :editor}) + + (let [project (th/create-project* 1 {:profile-id (:id editor) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id editor) + :project-id (:id project)})] + + (t/testing "remove member" + (let [out (th/command! {::th/type :delete-team-member + ::rpc/profile-id (:id owner) + :team-id (:id team) + :member-id (:id editor)})] + (t/is (th/success? out)))) + + (t/testing "renaming after removal fails with not-found" + (let [out (th/command! {::th/type :rename-file + ::rpc/profile-id (:id editor) + :id (:id file) + :name "renamed-again"}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))) + + (t/testing "file summary after removal fails with not-found" + (let [out (th/command! {::th/type :get-file-summary + ::rpc/profile-id (:id editor) + :id (:id file)}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found))))))) + +(t/deftest orphan-file-role-without-team-membership-grants-nothing + ;; Defense in depth: a file role row without a live team membership + ;; (leftover from before the leave-team cleanup) grants no access. + (let [owner (th/create-profile* 1 {:is-active true}) + stranger (th/create-profile* 2 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id owner)}) + project (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id project)})] + + ;; Plant an orphan file role for a profile that was never a member. + (th/create-file-role* {:file-id (:id file) + :profile-id (:id stranger) + :role :editor}) + + (t/testing "orphan file role cannot rename" + (let [out (th/command! {::th/type :rename-file + ::rpc/profile-id (:id stranger) + :id (:id file) + :name "renamed"}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))) + + (t/testing "orphan file role cannot read summary" + (let [out (th/command! {::th/type :get-file-summary + ::rpc/profile-id (:id stranger) + :id (:id file)}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))))) + +(t/deftest leaver-loses-project-access + ;; Same revocation at project level (covers sql:project-permissions). + (let [owner (th/create-profile* 1 {:is-active true}) + editor (th/create-profile* 2 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id owner)})] + + (th/create-team-role* {:team-id (:id team) + :profile-id (:id editor) + :role :editor}) + + (let [project (th/create-project* 1 {:profile-id (:id editor) + :team-id (:id team)})] + + (t/testing "leave team" + (let [out (th/command! {::th/type :leave-team + ::rpc/profile-id (:id editor) + :id (:id team)})] + (t/is (th/success? out)))) + + (t/testing "get-project after leaving fails with not-found" + (let [out (th/command! {::th/type :get-project + ::rpc/profile-id (:id editor) + :id (:id project)}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))) + + (t/testing "rename-project after leaving fails with not-found" + (let [out (th/command! {::th/type :rename-project + ::rpc/profile-id (:id editor) + :id (:id project) + :name "renamed"}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found))))))) + +(t/deftest leaver-vanishes-from-search + ;; The old team's files are no longer searchable by the leaver. + (let [owner (th/create-profile* 1 {:is-active true}) + editor (th/create-profile* 2 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id owner)})] + + (th/create-team-role* {:team-id (:id team) + :profile-id (:id editor) + :role :editor}) + + (let [project (th/create-project* 1 {:profile-id (:id editor) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id editor) + :project-id (:id project) + :name "searchable-file"})] + + (t/testing "member finds the file before leaving" + (let [out (th/command! {::th/type :search-files + ::rpc/profile-id (:id editor) + :team-id (:id team) + :search-term "searchable"})] + (t/is (th/success? out)) + (t/is (= (:id file) (-> out :result first :id))))) + + (t/testing "leave team" + (let [out (th/command! {::th/type :leave-team + ::rpc/profile-id (:id editor) + :id (:id team)})] + (t/is (th/success? out)))) + + (t/testing "search on the old team fails with not-found" + (let [out (th/command! {::th/type :search-files + ::rpc/profile-id (:id editor) + :team-id (:id team) + :search-term "searchable"}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found))))))) + +(t/deftest pins-are-cleaned-on-leave + ;; The project pin (team-project-profile-rel) goes with the leaver. + (let [owner (th/create-profile* 1 {:is-active true}) + editor (th/create-profile* 2 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id owner)})] + + (th/create-team-role* {:team-id (:id team) + :profile-id (:id editor) + :role :editor}) + + (let [project (th/create-project* 1 {:profile-id (:id editor) + :team-id (:id team)})] + + (t/testing "pin the project" + (let [out (th/command! {::th/type :update-project-pin + ::rpc/profile-id (:id editor) + :id (:id project) + :team-id (:id team) + :is-pinned true})] + (t/is (th/success? out))) + (t/is (= 1 (count (th/db-query :team-project-profile-rel + {:team-id (:id team) + :profile-id (:id editor)}))))) + + (t/testing "leave team removes the pin row" + (let [out (th/command! {::th/type :leave-team + ::rpc/profile-id (:id editor) + :id (:id team)})] + (t/is (th/success? out))) + (t/is (= 0 (count (th/db-query :team-project-profile-rel + {:team-id (:id team) + :profile-id (:id editor)})))))))) + +(t/deftest leaving-one-team-keeps-other-team-access + ;; Deletes are scoped by team: leaving A keeps B's files usable. + (let [owner (th/create-profile* 1 {:is-active true}) + editor (th/create-profile* 2 {:is-active true}) + team-a (th/create-team* 1 {:profile-id (:id owner)}) + team-b (th/create-team* 2 {:profile-id (:id owner)})] + + (th/create-team-role* {:team-id (:id team-a) + :profile-id (:id editor) + :role :editor}) + (th/create-team-role* {:team-id (:id team-b) + :profile-id (:id editor) + :role :editor}) + + (let [project-a (th/create-project* 1 {:profile-id (:id editor) + :team-id (:id team-a)}) + file-a (th/create-file* 1 {:profile-id (:id editor) + :project-id (:id project-a)}) + project-b (th/create-project* 2 {:profile-id (:id editor) + :team-id (:id team-b)}) + file-b (th/create-file* 2 {:profile-id (:id editor) + :project-id (:id project-b)})] + + (t/testing "leave team A" + (let [out (th/command! {::th/type :leave-team + ::rpc/profile-id (:id editor) + :id (:id team-a)})] + (t/is (th/success? out)))) + + (t/testing "team A file is gone" + (let [out (th/command! {::th/type :rename-file + ::rpc/profile-id (:id editor) + :id (:id file-a) + :name "renamed"}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))) + + (t/testing "team B file still works" + (let [out (th/command! {::th/type :rename-file + ::rpc/profile-id (:id editor) + :id (:id file-b) + :name "renamed"})] + (t/is (th/success? out))))))) + +(t/deftest orphan-project-role-grants-nothing-and-lists-nothing + ;; Mirror of the file orphan test at project level, including the + ;; get-all-projects listing (covers the F1 guard). + (let [owner (th/create-profile* 1 {:is-active true}) + stranger (th/create-profile* 2 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id owner)}) + project (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)})] + + ;; Plant an orphan project role for a profile that was never a member. + (th/create-project-role* {:project-id (:id project) + :profile-id (:id stranger) + :role :editor}) + + (t/testing "orphan project role cannot read the project" + (let [out (th/command! {::th/type :get-project + ::rpc/profile-id (:id stranger) + :id (:id project)}) + error (:error out)] + (t/is (th/ex-info? error)) + (t/is (th/ex-of-type? error :not-found)))) + + (t/testing "orphan project is not listed" + (let [out (th/command! {::th/type :get-all-projects + ::rpc/profile-id (:id stranger)})] + (t/is (th/success? out)) + (t/is (nil? (some #(= (:id project) (:id %)) (:result out)))))))) From 4b70c9aa210cbd70bb39283273ee551a226f924c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 5 Oct 2026 15:15:41 +0000 Subject: [PATCH 010/145] :bug: Reserve S3 download path without creating the file The S3 file download opens the temp file with CREATE_NEW, so it fails when the path already exists. tempfile creates the empty file on reserve, which made every large download depend on the SDK deleting the placeholder between its own retries. Add tmp/temp-path, which reserves a unique path and registers cleanup without creating the file, and use it for S3 downloads. The old retry on FileAlreadyExists stays as a safety net for a name clash. AI-assisted-by: muse-spark-1.3-contributor --- backend/src/app/storage/s3.clj | 2 +- backend/src/app/storage/tmp.clj | 19 +++++++++ .../test/backend_tests/storage_tmp_test.clj | 40 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 backend/test/backend_tests/storage_tmp_test.clj diff --git a/backend/src/app/storage/s3.clj b/backend/src/app/storage/s3.clj index 025749bee1..676660c95a 100644 --- a/backend/src/app/storage/s3.clj +++ b/backend/src/app/storage/s3.clj @@ -316,7 +316,7 @@ ;; to the filesystem and then read with buffered inputstream; if ;; not, read the contento into memory using bytearrays. (if (> ^long size (* 1024 1024 2)) - (let [path (tmp/tempfile :prefix "penpot.storage.s3." :min-age "6h") + (let [path (tmp/temp-path :prefix "penpot.storage.s3." :min-age "6h") rxf (AsyncResponseTransformer/toFile ^Path path)] (->> (.getObject ^S3AsyncClient client ^GetObjectRequest gor diff --git a/backend/src/app/storage/tmp.clj b/backend/src/app/storage/tmp.clj index b7a3076159..987ea4a1de 100644 --- a/backend/src/app/storage/tmp.clj +++ b/backend/src/app/storage/tmp.clj @@ -93,6 +93,25 @@ (sp/offer! queue [path (some-> min-age ct/duration)]) path)) +(defn temp-path + "Reserve a unique temp path WITHOUT creating the file. + + Use it when an external writer needs to create the file itself and + fails if it already exists (like the S3 SDK file download, which + opens with CREATE_NEW). The path is still registered for cleanup, + so a crash between reserve and write does not leak." + [& {:keys [suffix prefix dir min-age] + :or {prefix "penpot." + suffix ".tmp" + dir default-tmp-dir}}] + (fs/create-dir dir) + (loop [path (fs/join dir (str prefix (uuid/next) suffix))] + (if (fs/exists? path) + (recur (fs/join dir (str prefix (uuid/next) suffix))) + (do + (sp/offer! queue [path (some-> min-age ct/duration)]) + path)))) + (defn tempfile-from "Create a new tempfile from from consuming the stream" [input & {:as options}] diff --git a/backend/test/backend_tests/storage_tmp_test.clj b/backend/test/backend_tests/storage_tmp_test.clj new file mode 100644 index 0000000000..b9141cf20b --- /dev/null +++ b/backend/test/backend_tests/storage_tmp_test.clj @@ -0,0 +1,40 @@ +;; 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 SUBSIDIARY SL + +(ns backend-tests.storage-tmp-test + (:require + [app.storage.tmp :as tmp] + [clojure.test :as t] + [datoteka.fs :as fs]) + (:import + java.nio.file.Files + java.nio.file.Path)) + +(t/deftest temp-path-does-not-create-the-file + (let [path (tmp/temp-path :prefix "penpot.test." :min-age "6h")] + (t/testing "the reserved path does not exist yet" + (t/is (not (fs/exists? path)))) + (t/testing "an external writer can create it with CREATE_NEW" + (Files/createFile ^Path path (into-array java.nio.file.attribute.FileAttribute [])) + (t/is (fs/exists? path))) + (fs/delete path))) + +(t/deftest temp-path-is-unique + (let [a (tmp/temp-path :prefix "penpot.test.") + b (tmp/temp-path :prefix "penpot.test.")] + (t/is (not= a b)) + (t/is (not (fs/exists? a))) + (t/is (not (fs/exists? b))))) + +(t/deftest tempfile-creates-the-file-so-create-new-fails + (t/testing "documents why s3 downloads cannot use tempfile: the SDK opens with CREATE_NEW" + (let [path (tmp/tempfile :prefix "penpot.test.")] + (try + (t/is (fs/exists? path)) + (t/is (thrown? java.nio.file.FileAlreadyExistsException + (Files/createFile ^Path path (into-array java.nio.file.attribute.FileAttribute [])))) + (finally + (fs/delete path)))))) From 753e98bb9f2d2b0a1ff004a005ea130ff84263f3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 5 Oct 2026 16:23:52 +0000 Subject: [PATCH 011/145] :bug: Raise default binfile import text total to 500MiB Giant legitimate exports hold hundreds of MB of JSON across hundreds of thousands of KB-sized entries without tripping any per-entry cap, so the 200MiB cumulative default rejected them out of the box. Raise it to 500MiB; the 20MiB per-entry cap still bounds single-entry inflation. Pin the floor with a regression test. AI-assisted-by: muse-spark-1.3-contributor --- backend/src/app/binfile/common.clj | 10 +++++++--- backend/test/backend_tests/binfile_test.clj | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index 2909e35815..951b225818 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -68,11 +68,15 @@ (* 1024 1024 20)) ;; Maximum total decompressed size allowed for all JSON/text zip entries -;; combined within a single import job: 200MiB. Bounds the case where many +;; combined within a single import job: 500MiB. Bounds the case where many ;; entries, each individually under default-max-text-entry-size, still sum -;; to an unreasonable total. +;; to an unreasonable total. Set well above the 200MiB it replaces because +;; legitimate giant exports exist: hundreds of thousands of KB-sized +;; entries (pages, shapes) that never trip the per-entry cap but sum past +;; 400MiB. The per-entry cap still bounds single-entry DEFLATE +;; amplification, so this only moves the cumulative ceiling. (def ^:const default-max-text-total-size - (* 1024 1024 200)) + (* 1024 1024 500)) ;; Maximum number of entries allowed in the import zip: 500,000. (def ^:const default-max-zip-entries diff --git a/backend/test/backend_tests/binfile_test.clj b/backend/test/backend_tests/binfile_test.clj index 715def2ad2..0ffb1d2262 100644 --- a/backend/test/backend_tests/binfile_test.clj +++ b/backend/test/backend_tests/binfile_test.clj @@ -2153,6 +2153,13 @@ (t/is (= :validation (:type out))) (t/is (= :max-file-size-reached (:code out)))))) +(t/deftest default-text-total-budget-fits-giant-legit-files + ;; A real-world giant export held ~437 MiB of JSON across ~178k + ;; KB-sized entries without tripping any per-entry cap. The default + ;; cumulative budget must clear that scale, or such files cannot be + ;; imported out of the box. + (t/is (>= bfc/default-max-text-total-size (* 1024 1024 450)))) + (defn- text-entries-sizes "Returns the decompressed sizes of every `.json` entry in the zip at `zip-path`. Used to pick a cumulative budget that sits between the From b41445d092ec5c0bcf60be88cc4f739445dfbd61 Mon Sep 17 00:00:00 2001 From: breken Date: Mon, 5 Oct 2026 11:17:04 -0700 Subject: [PATCH 012/145] :bug: Keep background plugins registered when another plugin opens (#11929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loading a plugin closed every non-background plugin and then emptied the runtime registry, dropping background plugins (allowBackground, such as the MCP plugin) that were left running. The registry is what routes a plugin iframe's postMessage traffic to its plugin and what ɵunloadPlugin searches, so after the user opened any other plugin the MCP plugin never received task requests from its UI (MCP tool calls timed out) and could no longer be unloaded. Keep background plugins in the registry and drop only the plugins that were closed. AI-assisted-by: claude-opus-5.5 Signed-off-by: breken-ai <312387581+breken-ai@users.noreply.github.com> Co-authored-by: breken-ai <312387581+breken-ai@users.noreply.github.com> --- .serena/memories/plugins/core.md | 2 +- plugins/CHANGELOG.md | 1 + .../src/lib/load-plugin.spec.ts | 42 +++++++++++++++++++ .../plugins-runtime/src/lib/load-plugin.ts | 10 ++++- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/.serena/memories/plugins/core.md b/.serena/memories/plugins/core.md index b042c34aa8..64f4643860 100644 --- a/.serena/memories/plugins/core.md +++ b/.serena/memories/plugins/core.md @@ -26,7 +26,7 @@ ## Lifecycle -- Loading a plugin closes existing non-background plugins and resets the runtime registry. Be careful around `allowBackground` semantics when changing load/close behavior. +- Loading a plugin closes existing non-background plugins and removes them from the runtime registry. Background plugins (`allowBackground`) keep running and must stay registered: the registry routes their UI `postMessage` traffic by sender iframe and is what `ɵunloadPlugin` searches. - If sandbox evaluation fails, the runtime marks the error as plugin-originated, closes the plugin, and rethrows. - `plugin-manager` removes event listeners, timers, intervals, and modal state on close, and marks the plugin destroyed. Listener callbacks check that flag because Penpot events can fire after close. diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 5c1e07dd74..4b45df68b0 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -16,6 +16,7 @@ - **plugins-runtime**: Removed the premature deep-hardening of the host plugin context, which froze shared host functions (including `Function.prototype`) before SES override taming, causing `TypeError: Cannot assign to read only property 'toString'` on later host-side function extension. Related to #11001. - **plugins-runtime**: The plugin modal no longer shows two resize grips in Firefox. Firefox now shows only its native grip. Closes #11795. - **plugins-runtime**: Fixed the `fontFamilies` token property mapping so `Shape.applyToken(token, ["fontFamilies"])` resolves to the canonical `:font-family` attribute and applied-token readback exposes the documented `fontFamilies` key instead of the undocumented singular `fontFamily`. Closes #11405. +- **plugins-runtime**: Opening a plugin no longer drops running background plugins (`allowBackground`, such as the MCP plugin) from the runtime registry. They kept running, but their UI messages were no longer delivered to the plugin and they could no longer be unloaded, so MCP tasks timed out after another plugin was opened. ## 1.5.0 (2026-07-08) diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts index 810ebd54b3..779619279c 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts @@ -3,6 +3,7 @@ import { loadPlugin, ɵloadPlugin, ɵloadPluginByUrl, + ɵunloadPlugin, setContextBuilder, getPlugins, } from './load-plugin'; @@ -200,6 +201,47 @@ describe('plugin-loader', () => { expect(mockPluginApi1.plugin.sendMessage).not.toHaveBeenCalled(); }); + it('should keep background plugins registered when loading another plugin', async () => { + const backgroundIframeWindow = { nodeType: 1 } as unknown as Window; + const backgroundClose = vi.fn(); + const backgroundPluginApi = { + plugin: { + close: backgroundClose, + sendMessage: vi.fn(), + }, + iframeWindow: backgroundIframeWindow, + manifest: { + ...manifest, + pluginId: 'background-plugin', + allowBackground: true, + }, + } as unknown as Awaited>; + + vi.mocked(createPlugin).mockResolvedValue(backgroundPluginApi); + await loadPlugin(manifest); + + vi.mocked(createPlugin).mockResolvedValue(mockPluginApi); + await loadPlugin(manifest); + + expect(backgroundClose).not.toHaveBeenCalled(); + expect(getPlugins()).toContain(backgroundPluginApi); + + const event = new MessageEvent('message', { data: 'from-background' }); + Object.defineProperty(event, 'source', { value: backgroundIframeWindow }); + window.dispatchEvent(event); + + expect(backgroundPluginApi.plugin.sendMessage).toHaveBeenCalledWith( + 'from-background', + ); + + ɵunloadPlugin('background-plugin'); + expect(backgroundClose).toHaveBeenCalledTimes(1); + + // the runtime's close callback deregisters the plugin + vi.mocked(createPlugin).mock.calls[0][2](); + expect(getPlugins()).not.toContain(backgroundPluginApi); + }); + it('should load plugin using ɵloadPlugin', async () => { await ɵloadPlugin(manifest); diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts index dfd44f4971..e22e62c70c 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts @@ -17,14 +17,20 @@ export function setContextBuilder(builder: ContextBuilder) { export const getPlugins = () => plugins; const closeAllPlugins = () => { + // Background plugins keep running, so they must stay registered: the + // registry routes their UI messages and lets them be unloaded later. + const backgroundPlugins: typeof plugins = []; + plugins.forEach((pluginApi) => { /* eslint-disable @typescript-eslint/no-explicit-any */ - if (!(pluginApi.manifest as any)?.allowBackground) { + if ((pluginApi.manifest as any)?.allowBackground) { + backgroundPlugins.push(pluginApi); + } else { pluginApi.plugin.close(); } }); - plugins = []; + plugins = backgroundPlugins; }; window.addEventListener('message', (event) => { From 70dfe9057128a8dacb7118227b7c258c5805c362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Tue, 6 Oct 2026 10:34:47 +0200 Subject: [PATCH 013/145] :bug: Clear doc atlas when canvas background color changes (#12112) --- render-wasm/src/render.rs | 5 +++++ render-wasm/src/render/surfaces.rs | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index b840563f4f..cfc3ed1a0f 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -986,7 +986,12 @@ impl RenderState { } pub fn set_background_color(&mut self, color: skia::Color) { + if self.background_color == color { + return; + } self.background_color = color; + // Tiles bake the background in; old atlas pixels would show on pan/zoom. + self.surfaces.atlas.clear(); } pub fn set_preview_mode(&mut self, enabled: bool) { diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 761b2d02d6..9d07a6362e 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -185,6 +185,12 @@ impl DocAtlas { self.size.width <= 0 || self.size.height <= 0 } + /// Drops all atlas content while keeping its allocation and placement. + pub fn clear(&mut self) { + self.surface.canvas().clear(skia::Color::TRANSPARENT); + self.tile_doc_rects.clear(); + } + /// Sets the document-space bounds used to clamp atlas updates. /// Pass `None` to disable clamping. pub fn set_doc_bounds(&mut self, bounds: Option) { From 510173199b6591fc9af47bc9219243f665087a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Tue, 6 Oct 2026 11:31:52 +0200 Subject: [PATCH 014/145] :bug: Show 'Export complete' toast message after a successful export (#12118) --- frontend/src/app/main/data/exports/assets.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/data/exports/assets.cljs b/frontend/src/app/main/data/exports/assets.cljs index e1f8f8b508..27c0133cc4 100644 --- a/frontend/src/app/main/data/exports/assets.cljs +++ b/frontend/src/app/main/data/exports/assets.cljs @@ -193,7 +193,7 @@ (update :export assoc :in-progress false :last-update (ct/now) :healthy? healthy?) (= status "ended") - (update :export assoc :in-progress false :last-update (ct/now) :healthy? healthy?)))) + (update :export assoc :in-progress false :progress done :last-update (ct/now) :healthy? healthy?)))) ptk/WatchEvent (watch [_ _ _] From 3fe48727ea9bea2104e29908de07f5fa9dee6a3a Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 6 Oct 2026 12:54:39 +0200 Subject: [PATCH 015/145] :bug: Hide import file rows while the import error list is shown (#12121) When the import modal content moved to import-files-stage* (#9958), the "error list or file rows" if became a when followed by rows that always render. A non-.penpot file yields an analyze error entry with no name, so the modal showed an empty error row below the error list. Render the file rows only when the error list is not shown, as before. Closes #12116 AI-assisted-by: claude-opus-5-5 --- .../src/app/main/ui/dashboard/import.cljs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index f6962592b4..dd459da077 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -602,7 +602,7 @@ :class (stl/css :context-notification-error)} (tr "dashboard.import.import-error.disclaimer")]) - (when (or (= :import-error status) (and (= :analyze status) errors?)) + (if (or (= :import-error status) (and (= :analyze status) errors?)) [:div {:class (stl/css :import-error-disclaimer)} [:div (tr "dashboard.import.import-error.message1")] [:ul {:class (stl/css :import-error-list)} @@ -624,18 +624,18 @@ :else (tr "dashboard.import.import-error.unknown-error"))])]))] - [:div (tr "dashboard.import.import-error.message2")]]) + [:div (tr "dashboard.import.import-error.message2")]] - (for [entry entries] - [:> import-entry* {:edition edition - :key (dm/str (:uri entry) "/" (:file-id entry)) - :entry entry - :entries entries - :is-progress (= :import-progress status) - :on-edit on-edit - :on-change on-change - :on-delete on-delete - :can-be-deleted (> (count entries) 1)}]) + (for [entry entries] + [:> import-entry* {:edition edition + :key (dm/str (:uri entry) "/" (:file-id entry)) + :entry entry + :entries entries + :is-progress (= :import-progress status) + :on-edit on-edit + :on-change on-change + :on-delete on-delete + :can-be-deleted (> (count entries) 1)}])) (when (some? template) [:> import-entry* {:entry (assoc template :status status) From d70b754652521fc1bc15d8463ba88aa58d73d6fd Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 6 Oct 2026 13:03:38 +0200 Subject: [PATCH 016/145] :bug: Lay out token pills in nested groups side by side again (#12120) The nested group wrapper only turned into a wrapping flex row when it had a direct button child. Since the pills are wrapped in the DS tooltip (#11900), the button is no longer a direct child, so pills in nested groups were stacked one per row. Match pills as "any child that is not a group" instead, so the rule no longer depends on the pill's own markup. Also drop the .token-pill rules of this module: CSS modules prefix them with the token_tree scope, so they never matched the pill button. Closes #12099 AI-assisted-by: claude-opus-5-5 --- .../ui/workspace/tokens/management/token_tree.scss | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss index 87ef7f2e4d..ee686c5f8e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.scss @@ -22,10 +22,6 @@ margin-block-end: var(--sp-s); } } - - & .token-pill { - flex: 0 0 auto; - } } .node-parent { @@ -37,7 +33,9 @@ padding-inline-start: var(--sp-s); border-inline-start: $b-2 solid var(--color-background-quaternary); - &:has(> button) { + // Match pills as "not a group" so the rule does not depend on the + // pill's own markup, which lives in another module. + &:has(> :not(.node-parent)) { display: flex; flex-wrap: wrap; gap: var(--sp-xs); @@ -50,8 +48,4 @@ margin-block-end: var(--sp-s); } } - - & .token-pill { - flex: 0 0 auto; - } } From d47753dbb3fc91c857d1004e5052a487625ec7dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 6 Oct 2026 13:14:22 +0200 Subject: [PATCH 017/145] :wrench: Build Docker images on Renovate branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Barragán Merino --- .github/workflows/build-renovate.yml | 252 +++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 .github/workflows/build-renovate.yml diff --git a/.github/workflows/build-renovate.yml b/.github/workflows/build-renovate.yml new file mode 100644 index 0000000000..bb7b9d9b40 --- /dev/null +++ b/.github/workflows/build-renovate.yml @@ -0,0 +1,252 @@ +name: Renovate Docker Image Check + +# Triggered by pushes to Renovate branches, not by pull_request: this is a +# public repository and these jobs run on the self-hosted runners, so a PR +# from a fork must never be able to start them. Only users with write +# access can push here. +on: + push: + branches: + - 'renovate-*' + +permissions: {} + +concurrency: + group: renovate-image-check-${{ github.ref_name }} + cancel-in-progress: true + +env: + BUNDLE_CACHE: /var/tmp/penpot-bundle-cache + +jobs: + # ── 1. Pick an existing develop bundle and detect what changed ──────── + prepare: + name: Prepare + runs-on: penpot-standar-runner + timeout-minutes: 10 + permissions: + contents: read + outputs: + short_sha: ${{ steps.bundle.outputs.short_sha }} + devenv: ${{ steps.changes.outputs.devenv }} + imagemagick: ${{ steps.changes.outputs.imagemagick }} + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Find the develop commit this branch is based on + id: base + run: | + echo "sha=$(git merge-base HEAD origin/develop)" >> "$GITHUB_OUTPUT" + + - name: Detect changed images + id: changes + env: + BASE_SHA: ${{ steps.base.outputs.sha }} + run: | + changed() { + git diff --quiet "$BASE_SHA" HEAD -- "$1" && echo false || echo true + } + { + echo "devenv=$(changed docker/devenv)" + echo "imagemagick=$(changed docker/imagemagick)" + } >> "$GITHUB_OUTPUT" + + # Renovate only changes Dockerfiles and package manager versions, so + # the most recent develop bundle is enough to build the images. + - name: Find the latest develop bundle + id: bundle + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} + S3_BUCKET: ${{ secrets.S3_BUCKET }} + BASE_SHA: ${{ steps.base.outputs.sha }} + run: | + for SHA in $(git rev-list --first-parent --max-count=50 "$BASE_SHA"); do + SHORT_SHA=$(git rev-parse --short=12 "$SHA") + if aws s3api head-object --bucket "$S3_BUCKET" \ + --key "penpot-sha-${SHORT_SHA}.zip" > /dev/null 2>&1; then + echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" + echo "Using bundle \`penpot-sha-${SHORT_SHA}.zip\`" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + done + echo "::error::No bundle found for the last 50 develop commits" + exit 1 + + # ── 2. Production images, built from the PR Dockerfiles ─────────────── + images: + name: Build ${{ matrix.image }} + runs-on: penpot-standar-runner + timeout-minutes: 60 + needs: prepare + permissions: + contents: read + strategy: + fail-fast: false + max-parallel: 3 + matrix: + image: [backend, frontend, exporter, storybook, mcp] + + steps: + - name: Set common environment variables + env: + MATRIX_IMAGE: ${{ matrix.image }} + run: | + echo "DOCKER_CONFIG=${RUNNER_TEMP}/.docker-${GITHUB_RUN_ID}-${GITHUB_JOB}-${MATRIX_IMAGE}" >> "$GITHUB_ENV" + + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Login to Docker Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ${{ secrets.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Login to DockerHub Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + username: ${{ secrets.PUB_DOCKER_USERNAME }} + password: ${{ secrets.PUB_DOCKER_PASSWORD }} + + - name: Login to Docker Hardened Images registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: dhi.io + username: ${{ secrets.PUB_DOCKER_USERNAME }} + password: ${{ secrets.PUB_DOCKER_PASSWORD }} + + - name: Prepare Penpot bundle + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }} + S3_BUCKET: ${{ secrets.S3_BUCKET }} + SHORT_SHA: ${{ needs.prepare.outputs.short_sha }} + IMAGE: ${{ matrix.image }} + run: | + ZIP="$BUNDLE_CACHE/penpot-sha-${SHORT_SHA}.zip" + if [ ! -f "$ZIP" ]; then + mkdir -p "$BUNDLE_CACHE" + aws s3 cp "s3://${S3_BUCKET}/penpot-sha-${SHORT_SHA}.zip" "$ZIP.$$.tmp" + mv "$ZIP.$$.tmp" "$ZIP" + fi + pushd docker/images + unzip -q "$ZIP" "penpot/${IMAGE}/*" + mv "penpot/${IMAGE}" "bundle-${IMAGE}" + popd + + - name: Set up QEMU + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + with: + platforms: linux/amd64,linux/arm64 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Build Docker image + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: ./docker/images/ + file: ./docker/images/Dockerfile.${{ matrix.image }} + platforms: linux/amd64,linux/arm64 + push: false + cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache + + # ── 3. Devenv, only when the PR touches it ──────────────────────────── + devenv: + name: Build devenv + runs-on: penpot-standar-runner + timeout-minutes: 90 + needs: prepare + if: needs.prepare.outputs.devenv == 'true' + permissions: + contents: read + + steps: + - name: Set common environment variables + run: | + echo "DOCKER_CONFIG=${RUNNER_TEMP}/.docker-${GITHUB_RUN_ID}-${GITHUB_JOB}" >> "$GITHUB_ENV" + + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Login to DockerHub Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + username: ${{ secrets.PUB_DOCKER_USERNAME }} + password: ${{ secrets.PUB_DOCKER_PASSWORD }} + + - name: Login to Docker Hardened Images registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: dhi.io + username: ${{ secrets.PUB_DOCKER_USERNAME }} + password: ${{ secrets.PUB_DOCKER_PASSWORD }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + with: + platforms: linux/amd64,linux/arm64 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Build devenv image + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: ./docker/devenv/ + file: ./docker/devenv/Dockerfile + platforms: linux/amd64,linux/arm64 + push: false + cache-from: type=registry,ref=penpotapp/devenv:buildcache + + # ── 4. ImageMagick, only when the PR touches it ─────────────────────── + # amd64 only: compiling ImageMagick under QEMU takes too long for a check. + imagemagick: + name: Build imagemagick + runs-on: penpot-standar-runner + timeout-minutes: 60 + needs: prepare + if: needs.prepare.outputs.imagemagick == 'true' + permissions: + contents: read + + steps: + - name: Set common environment variables + run: | + echo "DOCKER_CONFIG=${RUNNER_TEMP}/.docker-${GITHUB_RUN_ID}-${GITHUB_JOB}" >> "$GITHUB_ENV" + + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Login to Docker Hardened Images registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: dhi.io + username: ${{ secrets.PUB_DOCKER_USERNAME }} + password: ${{ secrets.PUB_DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Build imagemagick image + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: ./docker/imagemagick/ + file: ./docker/imagemagick/Dockerfile + platforms: linux/amd64 + push: false From bcd46310823564f65064e24191b50e52118493b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 6 Oct 2026 10:55:58 +0000 Subject: [PATCH 018/145] :wrench: Add Renovate configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Barragán Merino --- renovate.json | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000000..bfe3f97ff5 --- /dev/null +++ b/renovate.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>penpot/renovate-config"], + "semanticCommits": "disabled", + "commitMessagePrefix": ":arrow_up:", + "commitMessageAction": "Update", + "pinDigest": { + "commitMessageAction": "Pin" + }, + "nvm": { + "enabled": false + }, + "customManagers": [ + { + "customType": "regex", + "description": "Keep .nvmrc in sync with the DHI Node image", + "managerFilePatterns": ["/^\\.nvmrc$/"], + "matchStrings": ["v(?\\d+\\.\\d+\\.\\d+)"], + "depNameTemplate": "dhi.io/node", + "datasourceTemplate": "docker", + "extractVersionTemplate": "^(?\\d+\\.\\d+\\.\\d+)-debian13$" + }, + { + "customType": "regex", + "description": "Keep the devenv Node in sync with the DHI Node image", + "managerFilePatterns": ["/^docker/devenv/Dockerfile$/"], + "matchStrings": ["NODE_VERSION=v(?\\d+\\.\\d+\\.\\d+)"], + "depNameTemplate": "dhi.io/node", + "datasourceTemplate": "docker", + "extractVersionTemplate": "^(?\\d+\\.\\d+\\.\\d+)-debian13$" + } + ], + "packageRules": [ + { + "description": "Node versions in plain text have no digest to pin", + "matchManagers": ["custom.regex"], + "matchDatasources": ["docker"], + "pinDigests": false + } + ] +} From dacfac17bb630a301a4ec715c417fe0d54ed70f5 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 6 Oct 2026 13:33:36 +0200 Subject: [PATCH 019/145] :bug: Show the top stroke width in the old render (#12093) * :bug: Show the top stroke width in the old render With the old render, the stroke width input showed "Mixed" when the four sides had different widths or tokens. The old render only draws the top side, and the input must show it as the global stroke width. Move the input value and token logic into two pure functions. When per side strokes are disabled, both return the top side: its width and its token, or no token when the top has none. Closes #12063 AI-assisted-by: claude-opus-5-5 * :bug: Add e2e tests for the stroke width input in both renders Cover the global stroke width input when the four sides differ. With the new render it shows Mixed. With the old render it shows the top side: its width, or its token when the top has one. The old render cannot create per side data from the UI, and switching the render reloads the page and drops the test state. The tests patch the existing stroke token fixture instead, appending the per side fields to a rectangle and a board. Relates to #12063 AI-assisted-by: claude-opus-5-5 --- .../ui/specs/tokens/stroke-per-side.spec.js | 122 ++++++++++++++++++ .../sidebar/options/rows/stroke_row.cljs | 83 +++++++----- .../frontend_tests/ui/stroke_menu_test.cljs | 39 ++++++ 3 files changed, 214 insertions(+), 30 deletions(-) diff --git a/frontend/playwright/ui/specs/tokens/stroke-per-side.spec.js b/frontend/playwright/ui/specs/tokens/stroke-per-side.spec.js index 61952665bc..50c558478d 100644 --- a/frontend/playwright/ui/specs/tokens/stroke-per-side.spec.js +++ b/frontend/playwright/ui/specs/tokens/stroke-per-side.spec.js @@ -1,3 +1,4 @@ +import { readFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage"; import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage"; @@ -54,6 +55,76 @@ async function unfoldStrokeWidth(page, workspace) { await unfoldTokenType(workspace.tokensSidebar, "stroke width"); } +const STROKE_TOKEN_FILE = "workspace/get-file-layout-stroke-token-json"; +const PER_SIDE_RECT = "Per side rect"; +const PER_SIDE_TOKEN_BOARD = "Per side token board"; + +// Transit fields appended at the end of a shape, so the earlier "^N" cache +// references stay valid; when a key repeats, the last value wins. +function perSideStrokeFields(name, top, appliedTokens = []) { + const stroke = [ + "^ ", + ...["~:stroke-style", "~:solid", "~:stroke-alignment", "~:inner"], + ...["~:stroke-color", "#000000", "~:stroke-opacity", 1], + ...["~:stroke-width", top, "~:stroke-width-top", top], + ...["~:stroke-width-right", 4, "~:stroke-width-bottom", 8], + ...["~:stroke-width-left", 16], + ]; + return [ + "~:name", + name, + "~:strokes", + [stroke], + "~:applied-tokens", + ["^ ", ...appliedTokens], + ]; +} + +// Serves the stroke token file with a rectangle and a board whose sides +// have different widths (top 2 or 20, right 4, bottom 8, left 16). Only the +// board has a token, `width-big`, on its top side. Both are top-level +// layers, so the layers panel shows them without unfolding anything. +async function mockPerSideStrokeFile(workspace) { + await workspace.mockGetFile(STROKE_TOKEN_FILE); + + const payload = JSON.parse( + await readFile(`playwright/data/${STROKE_TOKEN_FILE}`, "utf-8"), + ); + const [pageData] = Object.values(payload["~:data"]["~:pages-index"]); + const objects = pageData["~:objects"]["~#penpot/objects-map/v2"]; + + const patch = (id, fields) => { + const shape = objects[id]; + const extra = JSON.stringify(fields).slice(1, -1); + objects[id] = `${shape.slice(0, -2)},${extra}]]`; + }; + + patch( + "~u8506e3f3-e05b-807c-8007-6ceac380abc1", + perSideStrokeFields(PER_SIDE_RECT, 2), + ); + patch( + "~u6c65a5dc-fb40-8072-8007-6ce644905054", + perSideStrokeFields(PER_SIDE_TOKEN_BOARD, 20, [ + "~:stroke-width-top", + "width-big", + ]), + ); + + // The last matching route wins over the one set by mockGetFile. + await workspace.page.route(/get\-file\?/, (route) => + route.fulfill({ + status: 200, + contentType: "application/transit+json", + body: JSON.stringify(payload), + }), + ); +} + +function globalWidthInput(page) { + return strokeRow(page).getByRole("textbox", { name: GLOBAL }); +} + test.describe("Tokens: stroke per side", () => { test("per-side toggle is hidden when the feature flag is off", async ({ page, @@ -583,7 +654,58 @@ test.describe("Tokens: stroke per side (inspect)", () => { }); }); +test.describe("Tokens: stroke per side, global width input", () => { + test("shows Mixed for different sides with the new renderer", async ({ + page, + }) => { + const workspace = new WasmWorkspacePage(page); + await workspace.mockConfigFlags([ + "enable-stroke-per-side", + "enable-feature-token-input", + ]); + await workspace.setupEmptyFile(); + await mockPerSideStrokeFile(workspace); + await workspace.goToWorkspace(); + await workspace.waitForFirstRender(); + + await workspace.clickLeafLayer(PER_SIDE_RECT); + await expect(globalWidthInput(page)).toHaveAttribute("placeholder", MIXED); + + await workspace.clickLeafLayer(PER_SIDE_TOKEN_BOARD); + await expect(globalWidthInput(page)).toHaveAttribute("placeholder", MIXED); + }); +}); + test.describe("Tokens: stroke per side (non wasm)", () => { + test("the global width input shows the top side without the new renderer", async ({ + page, + }) => { + const workspace = new WorkspacePage(page); + await workspace.mockConfigFlags([ + "enable-stroke-per-side", + "enable-feature-token-input", + ]); + await workspace.setupEmptyFile(); + await mockPerSideStrokeFile(workspace); + await workspace.goToWorkspace(); + + // Top 2, other sides 4, 8 and 16: the old render draws the top side, + // so the input shows it instead of Mixed. + await workspace.clickLeafLayer(PER_SIDE_RECT); + await expect(globalWidthInput(page)).toHaveValue("2"); + await expect(globalWidthInput(page)).not.toHaveAttribute( + "placeholder", + MIXED, + ); + + // Only the top side has a token: the input shows that token. + await workspace.clickLeafLayer(PER_SIDE_TOKEN_BOARD); + await expect( + strokeRow(page).getByRole("button", { name: "width-big" }), + ).toHaveCount(1); + await expect(globalWidthInput(page)).toHaveCount(0); + }); + test("per-side toggle is disabled without the new renderer", async ({ page, }) => { diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs index dd774a3390..278626cab8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -28,6 +28,51 @@ [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) +(defn stroke-width-input-value + "Value for the global stroke width input: the width when every side is + equal, `:multiple` otherwise. When per side strokes are disabled (old + render) the input shows the top side, the one that render draws." + [stroke per-side-disabled] + (let [width (:stroke-width stroke) + top (d/nilv (:stroke-width-top stroke) width) + right (d/nilv (:stroke-width-right stroke) width) + bottom (d/nilv (:stroke-width-bottom stroke) width) + left (d/nilv (:stroke-width-left stroke) width)] + (cond + per-side-disabled + top + + (= top right bottom left) + width + + :else + :multiple))) + +(defn stroke-width-input-token + "Token for the global stroke width input: the token when every side has + the same one, `:multiple` when only some sides have a token, nil when no + side has one. When per side strokes are disabled (old render) the input + shows the top side token, like `stroke-width-input-value` does." + [applied-tokens per-side-disabled] + (let [top (get applied-tokens :stroke-width-top) + right (get applied-tokens :stroke-width-right) + bottom (get applied-tokens :stroke-width-bottom) + left (get applied-tokens :stroke-width-left) + per-side [top right bottom left] + per-side-has-token? (some some? per-side)] + (cond + per-side-disabled + top + + (and (every? some? per-side) (apply = per-side)) + top + + per-side-has-token? + :multiple + + :else + nil))) + (mf/defc stroke-row* [{:keys [index stroke @@ -114,35 +159,15 @@ (not per-side-disabled) (true? (get stroke-per-side [ids index]))) - all-sides-equal? - (mf/with-memo [stroke] - (let [width (:stroke-width stroke) - top (d/nilv (:stroke-width-top stroke) width) - right (d/nilv (:stroke-width-right stroke) width) - bottom (d/nilv (:stroke-width-bottom stroke) width) - left (d/nilv (:stroke-width-left stroke) width)] - (= top right bottom left))) + stroke-width-value + (mf/with-memo [stroke per-side-disabled] + (stroke-width-input-value stroke per-side-disabled)) - show-multiple-placeholder? (or per-side? (not all-sides-equal?)) + show-multiple-placeholder? (or per-side? (= :multiple stroke-width-value)) applied-token-width - (mf/with-memo [applied-tokens] - (let [top (get applied-tokens :stroke-width-top) - right (get applied-tokens :stroke-width-right) - bottom (get applied-tokens :stroke-width-bottom) - left (get applied-tokens :stroke-width-left) - - per-side [top right bottom left] - per-side-has-token? (some some? per-side)] - (cond - (and (every? some? per-side) (apply = per-side)) - (first per-side) - - per-side-has-token? - :multiple - - :else - nil))) + (mf/with-memo [applied-tokens per-side-disabled] + (stroke-width-input-token applied-tokens per-side-disabled)) per-side-toggle-label (if per-side-disabled @@ -409,9 +434,7 @@ :placeholder (if show-multiple-placeholder? (tr "settings.multiple") "--") - :value (if all-sides-equal? - (:stroke-width stroke) - :multiple)}] + :value stroke-width-value}] [:> select* {:default-selected (d/name stroke-alignment) :options stroke-alignment-options :variant "icon-only" @@ -445,7 +468,7 @@ :size "s"}] [:> deprecated-input/numeric-input* {:value (if show-multiple-placeholder? nil - stroke-width) + stroke-width-value) :min 0 :placeholder (if show-multiple-placeholder? (tr "settings.multiple") diff --git a/frontend/test/frontend_tests/ui/stroke_menu_test.cljs b/frontend/test/frontend_tests/ui/stroke_menu_test.cljs index e219333e99..b6fcc7407e 100644 --- a/frontend/test/frontend_tests/ui/stroke_menu_test.cljs +++ b/frontend/test/frontend_tests/ui/stroke_menu_test.cljs @@ -6,8 +6,10 @@ (ns frontend-tests.ui.stroke-menu-test (:require + [app.common.types.token :as cto] [app.config :as cf] [app.main.ui.workspace.sidebar.options.menus.stroke :as stroke] + [app.main.ui.workspace.sidebar.options.rows.stroke-row :as stroke-row] [clojure.test :as t :include-macros true])) (def ^:private per-side-flags (conj cf/flags :stroke-per-side)) @@ -54,3 +56,40 @@ (t/testing "disabled when the feature flag is off" (with-redefs [cf/flags (disj cf/flags :stroke-per-side)] (t/is (false? (stroke/per-side-stroke-enabled? true)))))) + +(def ^:private per-side-stroke + {:stroke-width 2 + :stroke-width-top 2 + :stroke-width-right 4 + :stroke-width-bottom 8 + :stroke-width-left 16}) + +(t/deftest stroke-width-input-value-test + (t/testing "shows the width when every side is equal" + (t/is (= 5 (stroke-row/stroke-width-input-value {:stroke-width 5} false))) + (t/is (= 5 (stroke-row/stroke-width-input-value + (stroke/stroke-width-all-attrs 5) false)))) + + (t/testing "shows multiple when the sides differ" + (t/is (= :multiple (stroke-row/stroke-width-input-value per-side-stroke false)))) + + (t/testing "shows the top side when per side strokes are disabled" + (t/is (= 2 (stroke-row/stroke-width-input-value per-side-stroke true))))) + +(t/deftest stroke-width-input-token-test + (t/testing "shows the token when every side has the same one" + (t/is (= "sw" (stroke-row/stroke-width-input-token + (zipmap cto/per-side-stroke-width-keys (repeat "sw")) false)))) + + (t/testing "shows multiple when only some sides have the token" + (t/is (= :multiple (stroke-row/stroke-width-input-token + {:stroke-width-top "sw"} false)))) + + (t/testing "shows no token when no side has one" + (t/is (nil? (stroke-row/stroke-width-input-token {} false)))) + + (t/testing "shows the top side token when per side strokes are disabled" + (t/is (= "sw" (stroke-row/stroke-width-input-token + {:stroke-width-top "sw" :stroke-width-left "other"} true))) + (t/is (nil? (stroke-row/stroke-width-input-token + {:stroke-width-left "other"} true))))) From 582f943228dffb62d8fa18465429a640ab79859f Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 6 Oct 2026 13:48:56 +0200 Subject: [PATCH 020/145] :bug: Show stroke width tokens in the inspect stroke panel (#12096) Since stroke widths can be set per side, stroke width tokens are stored in the four per-side attributes, not in :stroke-width. The inspect stroke panel still read :stroke-width for Border width and had no token lookup for the per-side widths, so it showed the raw value instead of the token. Read the token of each side for the per-side widths, and the token shared by every side for Border width. Files that are not migrated yet keep the token in :stroke-width, so Border width still falls back to it. Closes #12029 AI-assisted-by: claude-opus-5-5 --- .../main/ui/inspect/styles/panels/stroke.cljs | 44 ++++++++++++++----- frontend/test/frontend_tests/runner.cljs | 2 + .../ui/inspect_stroke_tokens_test.cljs | 43 ++++++++++++++++++ 3 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 frontend/test/frontend_tests/ui/inspect_stroke_tokens_test.cljs diff --git a/frontend/src/app/main/ui/inspect/styles/panels/stroke.cljs b/frontend/src/app/main/ui/inspect/styles/panels/stroke.cljs index 53ac6d5b8c..039cfd3ce4 100644 --- a/frontend/src/app/main/ui/inspect/styles/panels/stroke.cljs +++ b/frontend/src/app/main/ui/inspect/styles/panels/stroke.cljs @@ -42,20 +42,44 @@ :border-width :stroke-width :border-color :stroke-color}) -(defn- get-applied-tokens-in-shape - [shape-tokens property] - (let [border-prop (get shape-prop->stroke-prop property)] - (if border-prop - (get shape-tokens border-prop) - (get shape-tokens property)))) +(def ^:private per-side-prop->token-attr + {:border-block-start-width :stroke-width-top + :border-inline-end-width :stroke-width-right + :border-block-end-width :stroke-width-bottom + :border-inline-start-width :stroke-width-left}) + +(defn- all-sides-token-name + "Token applied to the four stroke width sides when they all share it." + [applied-tokens] + (let [sides (map #(get applied-tokens %) + [:stroke-width-top :stroke-width-right + :stroke-width-bottom :stroke-width-left])] + (when (and (every? some? sides) (apply = sides)) + (first sides)))) + +(defn stroke-property-token-name + "Name of the token applied to the stroke `property`, read from the shape + `applied-tokens`. Stroke width tokens are stored per side, so a per-side + property reads its side and `:border-width` reads the token shared by + every side. Files not migrated yet keep it in the old `:stroke-width`." + [applied-tokens property] + (cond + (contains? per-side-prop->token-attr property) + (get applied-tokens (get per-side-prop->token-attr property)) + + (= :border-width property) + (or (all-sides-token-name applied-tokens) + (get applied-tokens :stroke-width)) + + :else + (let [border-prop (get shape-prop->stroke-prop property)] + (get applied-tokens (or border-prop property))))) (defn- get-resolved-token "Get the resolved token for a specific property in a shape." [property shape resolved-tokens] - (let [shape-tokens (:applied-tokens shape) - applied-tokens-in-shape (get-applied-tokens-in-shape shape-tokens property) - token (get resolved-tokens applied-tokens-in-shape)] - token)) + (let [token-name (stroke-property-token-name (:applied-tokens shape) property)] + (get resolved-tokens token-name))) ;; Current token implementation on strokes only supports one token per shape and has to be the first stroke ;; This must be improved in the future diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 1f78325a70..865303404e 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -106,6 +106,7 @@ [frontend-tests.ui.ds-controls-numeric-input-test] [frontend-tests.ui.gradient-handlers-test] [frontend-tests.ui.history-test] + [frontend-tests.ui.inspect-stroke-tokens-test] [frontend-tests.ui.layout-container-multiple-test] [frontend-tests.ui.measures-menu-props-test] [frontend-tests.ui.organization-team-switch-test] @@ -239,6 +240,7 @@ 'frontend-tests.ui.ds-controls-numeric-input-test 'frontend-tests.ui.gradient-handlers-test 'frontend-tests.ui.history-test + 'frontend-tests.ui.inspect-stroke-tokens-test 'frontend-tests.ui.layout-container-multiple-test 'frontend-tests.ui.measures-menu-props-test 'frontend-tests.ui.organization-team-switch-test diff --git a/frontend/test/frontend_tests/ui/inspect_stroke_tokens_test.cljs b/frontend/test/frontend_tests/ui/inspect_stroke_tokens_test.cljs new file mode 100644 index 0000000000..9c7638a2dd --- /dev/null +++ b/frontend/test/frontend_tests/ui/inspect_stroke_tokens_test.cljs @@ -0,0 +1,43 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.ui.inspect-stroke-tokens-test + (:require + [app.common.types.token :as cto] + [app.main.ui.inspect.styles.panels.stroke :as stroke] + [cljs.test :as t :include-macros true])) + +(def ^:private all-sides-sw-3 + (zipmap cto/per-side-stroke-width-keys (repeat "sw-3"))) + +(t/deftest border-width-shows-the-token-applied-to-every-side + (t/is (= "sw-3" (stroke/stroke-property-token-name all-sides-sw-3 :border-width)))) + +(t/deftest border-width-shows-no-token-when-the-sides-differ + (t/is (nil? (stroke/stroke-property-token-name + (assoc all-sides-sw-3 :stroke-width-top "sw-10") :border-width))) + (t/is (nil? (stroke/stroke-property-token-name + {:stroke-width-top "sw-3"} :border-width)))) + +(t/deftest border-width-reads-the-legacy-key-of-unmigrated-files + (t/is (= "sw-3" (stroke/stroke-property-token-name {:stroke-width "sw-3"} :border-width)))) + +(t/deftest per-side-widths-show-the-token-of-each-side + (let [applied {:stroke-width-top "sw-10" + :stroke-width-right "sw-3" + :stroke-width-bottom "sw-3" + :stroke-width-left "sw-4"}] + (t/is (= "sw-10" (stroke/stroke-property-token-name applied :border-block-start-width))) + (t/is (= "sw-3" (stroke/stroke-property-token-name applied :border-inline-end-width))) + (t/is (= "sw-3" (stroke/stroke-property-token-name applied :border-block-end-width))) + (t/is (= "sw-4" (stroke/stroke-property-token-name applied :border-inline-start-width))))) + +(t/deftest per-side-widths-without-a-token-show-none + (t/is (nil? (stroke/stroke-property-token-name + {:stroke-width-top "sw-10"} :border-inline-end-width)))) + +(t/deftest border-color-keeps-the-stroke-color-token + (t/is (= "red" (stroke/stroke-property-token-name {:stroke-color "red"} :border-color)))) From ff74c38bbf0b01dc540be64747dec91b98e35ef6 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 6 Oct 2026 13:49:16 +0200 Subject: [PATCH 021/145] :bug: Keep per-side token attributes when remapping a token (#12091) Renaming a token with "Remap tokens" grouped the references by container and wrote the union of their attributes onto every shape in it. A shape with a stroke width token on one side got the token on all four sides when another shape in the same container had it on every side. Group the references by shape, so each shape is only renamed on the attributes it already had the token on. Closes #12075 AI-assisted-by: claude-opus-5-5 --- .../main/data/workspace/tokens/remapping.cljs | 18 ++++++---- .../tokens/logic/token_remapping_test.cljs | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/main/data/workspace/tokens/remapping.cljs b/frontend/src/app/main/data/workspace/tokens/remapping.cljs index e36b9d1b1d..7ff4a8be4b 100644 --- a/frontend/src/app/main/data/workspace/tokens/remapping.cljs +++ b/frontend/src/app/main/data/workspace/tokens/remapping.cljs @@ -115,11 +115,14 @@ shape-changes (reduce-kv (fn [changes container refs] - (let [shape-ids (map :shape-id refs) + (let [;; Each shape keeps only the attributes it already had the + ;; token on, so the rename never applies the token to new + ;; attributes (e.g. a stroke-width token on a single side). + attributes-by-shape (update-vals (group-by :shape-id refs) + #(into #{} (map :attribute) %)) ;; Find the correct token to apply (new or alias) - token (or (some #(when (= (:name (:token %)) new-token-name) %) tokens-with-sets) - (some #(when (= (:name (:token %)) old-token-name) %) tokens-with-sets)) - attributes (set (map :attribute refs))] + token (or (some #(when (= (:name (:token %)) new-token-name) %) tokens-with-sets) + (some #(when (= (:name (:token %)) old-token-name) %) tokens-with-sets))] (if token ;; Create a new independent changes so we can call `with-file-data` after `with-container` ;; otherwise it causes probelms looking up for objects @@ -127,10 +130,11 @@ (-> (pcb/empty-changes) (pcb/with-container container) (pcb/with-file-data file-data) - (pcb/update-shapes shape-ids + (pcb/update-shapes (keys attributes-by-shape) (fn [shape] - (update shape :applied-tokens - #(merge % (cft/attributes-map attributes (:token token))))) + (let [attributes (get attributes-by-shape (:id shape))] + (update shape :applied-tokens + #(merge % (cft/attributes-map attributes (:token token)))))) {:ignore-touched true}))] (pcb/concat-changes changes container-changes)) changes))) diff --git a/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs index acad25311c..fc47db9132 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs @@ -120,6 +120,39 @@ (let [result (dwtr/validate-token-remapping "color.primary" "brand.primary")] (t/is (true? (:valid? result))))))) +(t/deftest test-remap-keeps-applied-attributes-per-shape + (t/testing "renaming a token only renames the attributes each shape already had" + (let [stroke-token {:id (cthi/new-id! :sw-10) + :name "sw-10" + :value "10" + :type :stroke-width} + file (-> (ctht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a) + :name "Set A")) + (ctob/add-token (cthi/id :set-a) + (ctob/make-token stroke-token)))) + (ctho/add-rect :top-rect) + (ctho/add-rect :all-rect) + (ctht/apply-token-to-shape :top-rect "sw-10" + [:stroke-width-top] [] nil) + (ctht/apply-token-to-shape :all-rect "sw-10" + cto/stroke-width-keys [] nil) + ;; The production flow renames the token first, + ;; then remaps the shapes that use it. + (update-in [:data :tokens-lib] + ctob/update-token (cthi/id :set-a) (cthi/id :sw-10) + #(assoc % :name "sw-big"))) + changes (dwtr/build-remap-changes (:data file) "sw-10" "sw-big") + file' (cthf/apply-changes file changes) + top-rect' (cths/get-shape file' :top-rect) + all-rect' (cths/get-shape file' :all-rect)] + + (t/is (= {:stroke-width-top "sw-big"} + (:applied-tokens top-rect'))) + (t/is (= (zipmap cto/stroke-width-keys (repeat "sw-big")) + (:applied-tokens all-rect')))))) + (defn- setup-file-with-component-copy-and-token "Create a file containing a component, an instance copy of it, and a single `color.primary` token already applied to both the main and the copy shape. From e17c3c0c8fcfc0577d07bdcf99e1a9e10eb9818f Mon Sep 17 00:00:00 2001 From: "penpot-renovate[bot]" <338182062+penpot-renovate[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:26:54 +0000 Subject: [PATCH 022/145] :arrow_up: Pin DHI base image digests - dhi.io/debian-base trixie-debian13-dev@61dc022 (docker/images/Dockerfile.backend) - dhi.io/debian-base trixie-debian13-dev@61dc022 (docker/images/Dockerfile.backend) - dhi.io/debian-base trixie-debian13@20079b5 (docker/imagemagick/Dockerfile) - dhi.io/debian-base trixie-debian13-dev@61dc022 (docker/imagemagick/Dockerfile) - dhi.io/debian-base trixie-debian13-dev@61dc022 (docker/devenv/Dockerfile) - dhi.io/nginx 1.31.6-alpine3.24@9465275 (docker/images/Dockerfile.storybook) - dhi.io/nginx 1.31.6-debian13-dev@32e4340 (docker/images/Dockerfile.frontend) - dhi.io/node 24.21.0-debian13-dev@2e51e83 (docker/images/Dockerfile.media-processor) - dhi.io/node 24.21.0-debian13@f3575cf (docker/images/Dockerfile.mcp) - dhi.io/node 24.21.0-debian13-dev@2e51e83 (docker/images/Dockerfile.mcp) - dhi.io/node 24.21.0-debian13-dev@2e51e83 (docker/images/Dockerfile.exporter) Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com> --- docker/devenv/Dockerfile | 2 +- docker/imagemagick/Dockerfile | 4 ++-- docker/images/Dockerfile.backend | 4 ++-- docker/images/Dockerfile.exporter | 2 +- docker/images/Dockerfile.frontend | 2 +- docker/images/Dockerfile.mcp | 4 ++-- docker/images/Dockerfile.media-processor | 2 +- docker/images/Dockerfile.storybook | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 3d841a45be..b198c875a1 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -1,4 +1,4 @@ -FROM dhi.io/debian-base:trixie-debian13-dev AS base +FROM dhi.io/debian-base:trixie-debian13-dev@sha256:61dc022a1908439c478ed4b00e883fe8a7a03540fac23a8aed61352e28e6ae07 AS base ENV LANG='C.UTF-8' \ LC_ALL='C.UTF-8' \ diff --git a/docker/imagemagick/Dockerfile b/docker/imagemagick/Dockerfile index 0ab72b37af..157897ca6e 100644 --- a/docker/imagemagick/Dockerfile +++ b/docker/imagemagick/Dockerfile @@ -1,4 +1,4 @@ -FROM dhi.io/debian-base:trixie-debian13-dev AS build +FROM dhi.io/debian-base:trixie-debian13-dev@sha256:61dc022a1908439c478ed4b00e883fe8a7a03540fac23a8aed61352e28e6ae07 AS build LABEL maintainer="Penpot " ENV LANG='C.UTF-8' \ @@ -74,7 +74,7 @@ RUN set -ex; \ rm -rf /var/lib/apt/lists/* -FROM dhi.io/debian-base:trixie-debian13 AS image +FROM dhi.io/debian-base:trixie-debian13@sha256:20079b51710f0397da5e056bfc7156b6aafc7ff3aa4ef4cbcb0b0f1df99cd8c4 AS image LABEL maintainer="Penpot " ENV LANG='C.UTF-8' \ diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend index e2f1f15179..bf4a33b5cf 100644 --- a/docker/images/Dockerfile.backend +++ b/docker/images/Dockerfile.backend @@ -1,4 +1,4 @@ -FROM dhi.io/debian-base:trixie-debian13-dev AS build +FROM dhi.io/debian-base:trixie-debian13-dev@sha256:61dc022a1908439c478ed4b00e883fe8a7a03540fac23a8aed61352e28e6ae07 AS build LABEL maintainer="Penpot " ENV LANG='C.UTF-8' \ @@ -53,7 +53,7 @@ RUN set -eux; \ --output /opt/jre; -FROM dhi.io/debian-base:trixie-debian13-dev AS image +FROM dhi.io/debian-base:trixie-debian13-dev@sha256:61dc022a1908439c478ed4b00e883fe8a7a03540fac23a8aed61352e28e6ae07 AS image LABEL maintainer="Penpot " ENV LANG='C.UTF-8' \ diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 1153014be3..14eb31f211 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -1,4 +1,4 @@ -FROM dhi.io/node:24.21.0-debian13-dev +FROM dhi.io/node:24.21.0-debian13-dev@sha256:2e51e8397b85c3771c554834fb55d7fd5825e7d57a993e00153db42c584b7e58 LABEL maintainer="Penpot " ENV LANG=en_US.UTF-8 \ diff --git a/docker/images/Dockerfile.frontend b/docker/images/Dockerfile.frontend index 7c6547779e..6e913f5919 100644 --- a/docker/images/Dockerfile.frontend +++ b/docker/images/Dockerfile.frontend @@ -1,4 +1,4 @@ -FROM dhi.io/nginx:1.31.6-debian13-dev +FROM dhi.io/nginx:1.31.6-debian13-dev@sha256:32e434084ff7e82629abc090c534fa0b02ebf1a7465f8029314bf9e95349c3a1 LABEL maintainer="Penpot " ENV DEBIAN_FRONTEND=noninteractive diff --git a/docker/images/Dockerfile.mcp b/docker/images/Dockerfile.mcp index 1394306bd1..f95a6869b5 100644 --- a/docker/images/Dockerfile.mcp +++ b/docker/images/Dockerfile.mcp @@ -1,4 +1,4 @@ -FROM dhi.io/node:24.21.0-debian13-dev AS build +FROM dhi.io/node:24.21.0-debian13-dev@sha256:2e51e8397b85c3771c554834fb55d7fd5825e7d57a993e00153db42c584b7e58 AS build LABEL maintainer="Penpot " ENV DEBIAN_FRONTEND=noninteractive @@ -42,7 +42,7 @@ RUN set -eux; \ RUN ./setup -FROM dhi.io/node:24.21.0-debian13 AS image +FROM dhi.io/node:24.21.0-debian13@sha256:f3575cf1e68ffe163703b691c465af833fd2d94cccc1e825d052c943018f2aa3 AS image LABEL maintainer="Penpot " ENV LANG=en_US.UTF-8 \ diff --git a/docker/images/Dockerfile.media-processor b/docker/images/Dockerfile.media-processor index fbabbfb70f..4b16df1ca0 100644 --- a/docker/images/Dockerfile.media-processor +++ b/docker/images/Dockerfile.media-processor @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM dhi.io/node:24.21.0-debian13-dev +FROM dhi.io/node:24.21.0-debian13-dev@sha256:2e51e8397b85c3771c554834fb55d7fd5825e7d57a993e00153db42c584b7e58 LABEL maintainer="Penpot " ENV LANG=en_US.UTF-8 \ diff --git a/docker/images/Dockerfile.storybook b/docker/images/Dockerfile.storybook index 7428803510..13a887fb0a 100644 --- a/docker/images/Dockerfile.storybook +++ b/docker/images/Dockerfile.storybook @@ -1,4 +1,4 @@ -FROM dhi.io/nginx:1.31.6-alpine3.24 +FROM dhi.io/nginx:1.31.6-alpine3.24@sha256:94652754aa6bbf187859b24c0b547aa2c3d07e99d4462868f8d9f36bf2039d88 LABEL maintainer="Penpot " ARG BUNDLE_PATH="./bundle-storybook/" From 10504ff2bfc71f9f858c50bb268b97399ac60a2b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 6 Oct 2026 14:21:12 +0200 Subject: [PATCH 023/145] :bug: Sanitize SVG on binfile import (#12105) * :bug: Sanitize smuggled SVG on binfile import Route imported SVG storage objects through the SVG sanitizer on all three binfile paths (v3, v1, v2), closing the second bypass of GHSA-ffhp-m958-qxvr. Integrity checks still run on the raw bundle bytes first; only the persisted copy is the sanitized one. Adds a shared sanitize-imported-svg helper with unit tests and a v3 export-tamper-import test proving smuggled scripts no longer survive the import. AI-assisted-by: muse-spark-1.3-contributor * :bug: Normalize SVG content-type on binfile import Close the residual GHSA-ffhp-m958-qxvr bypass where a crafted content-type spelling (uppercase, parameters) skipped the import sanitizer. Detection now uses a shared case-insensitive predicate, stored values are canonicalized, and the v3 re-read honors the import size limit. Extends the tamper tests to obfuscated spellings and adds exhaustive v1 coverage, including the tempfile branch. AI-assisted-by: muse-spark-1.3-contributor * :bug: Gate binfile content-type on known storage types Define the complete set of storage content-types in app.common.media and enforce it on the binfile import schema, so unknown types fail closed instead of passing through. Detection keeps a single normalization at the boundary with an exact predicate, and the string helpers use cuerdas. AI-assisted-by: muse-spark-1.3-contributor * :bug: Keep only produced types in storage-object-types Drop apng, avif, penpot and plain text from the set: no flow stores them, they only exist in the extension mapping table. Every member must have a producing code path; unproduced types stay out until some flow actually stores them. AI-assisted-by: muse-spark-1.3-contributor * :bug: Reject unknown content-type on v1 binfile import Enforce the storage content-type allowlist on the v1 import path, like v3 already does via schema. Bundles declaring types outside the set, or none at all, fail closed with the same media-type-not-allowed error as uploads. The sanitize branch is explicit: SVG bytes are sanitized, anything else passes through. AI-assisted-by: muse-spark-1.3-contributor --- backend/src/app/binfile/common.clj | 73 +++ backend/src/app/binfile/v1.clj | 12 +- backend/src/app/binfile/v2.clj | 12 +- backend/src/app/binfile/v3.clj | 15 +- .../test/backend_tests/binfile_svg_test.clj | 425 ++++++++++++++++++ common/src/app/common/media.cljc | 17 + common/test/common_tests/media_test.cljc | 7 + 7 files changed, 556 insertions(+), 5 deletions(-) create mode 100644 backend/test/backend_tests/binfile_svg_test.clj diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index e2aed9a837..7fe8e6a532 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -15,6 +15,7 @@ [app.common.files.migrations :as fmg] [app.common.files.validate :as fval] [app.common.logging :as l] + [app.common.media :as cm] [app.common.schema :as sm] [app.common.time :as ct] [app.common.types.file :as ctf] @@ -27,6 +28,7 @@ [app.features.file-migrations :as fmigr] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] + [app.media.svg :as svg] [app.storage :as sto] [app.util.blob :as blob] [app.util.pointer-map :as pmap] @@ -898,3 +900,74 @@ (cons (:id file))) load-fn #(get-file cfg % :migrate? false)] (weak/loadable-weak-value-map library-ids load-fn {id file}))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SVG IMPORT SANITIZATION +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def svg-content-type + "Canonical content type of SVG media objects." + "image/svg+xml") + +(defn normalize-content-type + "Canonicalize a stored content-type: lowercase, trimmed, + parameters after `;` dropped. Returns nil for missing or unusable + values so ancient bundle entries without content-type keep passing + through untouched." + [ctype] + (when (string? ctype) + (let [clean (-> ctype + (str/split #";" 2) + (first) + (str/trim) + (str/lower))] + (when-not (str/empty? clean) + clean)))) + +(defn svg-object? + "True when the storage `object` claims the canonical SVG content + type. Expects an already normalized object (see + `normalize-content-type`): every import path normalizes the + metadata right after reading it, so the comparison stays an exact + match in a single place." + [object] + (= svg-content-type (:content-type object))) + +(def schema:content-type + "Typed content-type for binfile storage objects: a member of + `cm/storage-object-types`. Values are canonicalized with + `normalize-content-type` before validation, so legacy spellings + keep importing while unknown types are rejected." + [::sm/one-of {:format :string} cm/storage-object-types]) + +(defn check-storage-content-type + "Check an imported storage `object` (already normalized, see + `normalize-content-type`) against `cm/storage-object-types`. + Returns nil when the type is allowed; raises `:type :validation` + with `:code :media-type-not-allowed` (same as the upload path) + when it is missing or unknown, so crafted bundles fail closed." + [object] + (when-not (contains? cm/storage-object-types (:content-type object)) + (ex/raise :type :validation + :code :media-type-not-allowed + :hint "storage object declares an unknown content-type" + :content-type (:content-type object)))) + +(defn sanitize-imported-svg + "Sanitize the raw `bytes` of an imported storage `object` when it + holds an SVG document. Expects an already normalized object (see + `normalize-content-type`); returns nil when the object is not an + SVG. + + Otherwise returns a map with the sanitized `:bytes`, their `:size` + and their blake2b `:hash`, ready to persist with `sto/put-object!`. + + Raises a `:validation` exception when the SVG cannot be parsed, the + same error the upload path reports." + [object ^bytes raw] + (when (svg-object? object) + (let [sanitized (svg/sanitize-svg (String. ^bytes raw "UTF-8")) + bytes (.getBytes ^String sanitized "UTF-8")] + {:bytes bytes + :size (alength ^bytes bytes) + :hash (sto/calculate-hash (java.io.ByteArrayInputStream. bytes))}))) diff --git a/backend/src/app/binfile/v1.clj b/backend/src/app/binfile/v1.clj index be65b423c9..fd340a487d 100644 --- a/backend/src/app/binfile/v1.clj +++ b/backend/src/app/binfile/v1.clj @@ -607,16 +607,26 @@ (doseq [expected-storage-id ids] (let [id (read-uuid! input) - mdata (read-obj! input)] + mdata (d/update-when (read-obj! input) :content-type bfc/normalize-content-type)] (when (not= id expected-storage-id) (ex/raise :type :validation :code :inconsistent-penpot-file :hint "the penpot file seems corrupt, found unexpected uuid (storage-object-id)")) + (bfc/check-storage-content-type mdata) + (l/dbg :hint "readed storage object" :id (str id) ::l/sync? true) (let [[size resource] (read-stream! input) + [resource size] (if (bfc/svg-object? mdata) + (let [raw (if (bytes? resource) + resource + (with-open [istream (jio/input-stream resource)] + (io/read istream))) + {:keys [bytes size]} (bfc/sanitize-imported-svg mdata raw)] + [bytes size]) + [resource size]) hash (sto/calculate-hash resource) content (-> (sto/content resource size) (sto/wrap-with-hash hash)) diff --git a/backend/src/app/binfile/v2.clj b/backend/src/app/binfile/v2.clj index 347074586b..2e0d04107a 100644 --- a/backend/src/app/binfile/v2.clj +++ b/backend/src/app/binfile/v2.clj @@ -197,9 +197,17 @@ (defn- read-storage-object! [{:keys [::sto/storage ::bfc/timestamp] :as cfg} id] - (let [mdata (read-obj cfg :storage-object id) + (let [mdata (d/update-when (read-obj cfg :storage-object id) + :content-type bfc/normalize-content-type) data (read-blob cfg :storage-object id) - hash (sto/calculate-hash data) + ;; NOTE: v2 bundles are legacy and no longer produced; the + ;; sanitizer call stays as defense in depth and is covered by + ;; the shared helper unit tests, with no dedicated v2 + ;; import test. + clean (when (bfc/svg-object? mdata) + (bfc/sanitize-imported-svg mdata data)) + data (or (:bytes clean) data) + hash (or (:hash clean) (sto/calculate-hash data)) content (-> (sto/content data) (sto/wrap-with-hash hash)) diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index cc488f6ab8..cdc3dd66bb 100644 --- a/backend/src/app/binfile/v3.clj +++ b/backend/src/app/binfile/v3.clj @@ -74,7 +74,7 @@ [:map {:title "StorageObject"} [:id ::sm/uuid] [:size ::sm/int] - [:content-type :string] + [:content-type bfc/schema:content-type] [:bucket [::sm/one-of {:format :string} sto/valid-buckets]] [:hash {:optional true} :string]]) @@ -871,6 +871,7 @@ (doseq [{:keys [id entry]} entries] (let [object (-> (read-entry input entry) (decode-storage-object) + (d/update-when :content-type bfc/normalize-content-type) (update :bucket d/nilv sto/default-bucket) (validate-storage-object)) @@ -906,7 +907,17 @@ :expected-hash (:hash object) :found-hash (sto/get-hash content)))) - (let [params (-> object + (let [clean (when (bfc/svg-object? object) + (let [limit (::bfc/import-max-object-size cfg) + raw (with-open [istream (cond-> (zip-entry-stream input (get-zip-entry input path)) + limit (size-limiting-stream limit))] + (io/read istream))] + (bfc/sanitize-imported-svg object raw))) + content (if-let [{:keys [bytes hash]} clean] + (-> (sto/content bytes) + (sto/wrap-with-hash hash)) + content) + params (-> object (dissoc :id :size) (assoc ::sto/content content) (assoc ::sto/deduplicate? true) diff --git a/backend/test/backend_tests/binfile_svg_test.clj b/backend/test/backend_tests/binfile_svg_test.clj new file mode 100644 index 0000000000..8b63de4ab8 --- /dev/null +++ b/backend/test/backend_tests/binfile_svg_test.clj @@ -0,0 +1,425 @@ +;; 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 Sucursal en España SL + +(ns backend-tests.binfile-svg-test + "SVG sanitization on the binfile import path. + + Second case of GHSA-ffhp-m958-qxvr: a crafted `.penpot` bundle can + smuggle an unsanitized SVG storage object past the upload-path + sanitizer, and the raw bytes are served same-origin as + `image/svg+xml`." + (:require + [app.binfile.common :as bfc] + [app.binfile.v1 :as v1] + [app.binfile.v3 :as v3] + [app.common.features :as cfeat] + [app.common.types.shape :as cts] + [app.common.uuid :as uuid] + [app.db :as db] + [app.rpc :as-alias rpc] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [backend-tests.helpers :as th] + [clojure.data.json :as json] + [clojure.java.io :as jio] + [clojure.string :as str] + [clojure.test :as t] + [datoteka.fs :as fs] + [datoteka.io :as io]) + (:import + java.io.ByteArrayInputStream + java.util.zip.ZipEntry + java.util.zip.ZipFile + java.util.zip.ZipOutputStream)) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(def ^:private evil-svg + "") + +(def ^:private evil-xhref-svg + "click") + +(def ^:private clean-svg + "") + +(defn- utf8bytes + [^String s] + (.getBytes s "UTF-8")) + +(defn- bytes-str + [^bytes data] + (String. data "UTF-8")) + +(t/deftest sanitize-imported-svg-removes-script + (let [result (bfc/sanitize-imported-svg {:content-type "image/svg+xml" :bucket "file-media-object"} + (utf8bytes evil-svg))] + (t/is (some? result)) + (t/is (not (str/includes? (bytes-str (:bytes result)) "")))))) + +(t/deftest check-storage-content-type-allows-known-types + (doseq [ctype ["image/svg+xml" "image/jpeg" "font/woff2" "application/octet-stream"]] + (t/is (nil? (bfc/check-storage-content-type {:content-type ctype})) + (str "expected pass for " (pr-str ctype))))) + +(t/deftest check-storage-content-type-rejects-unknown-types + (doseq [ctype ["text/html" "application/x-font-woff" nil "" " "]] + (let [object (if (nil? ctype) {} {:content-type ctype}) + out (try (bfc/check-storage-content-type object) + nil + (catch clojure.lang.ExceptionInfo e + (ex-data e)))] + (t/is (= :validation (:type out)) (str "expected validation for " (pr-str ctype))) + (t/is (= :media-type-not-allowed (:code out)) (str "expected media-type-not-allowed for " (pr-str ctype)))))) + +(t/deftest storage-object-schema-constrains-content-type + (t/testing "known stored types validate, unknown types do not" + (let [base {:id (uuid/random) :size 10 :bucket "file-media-object"}] + (doseq [ctype ["image/svg+xml" "image/jpeg" "image/png" + "font/woff2" "font/ttf" "application/octet-stream" "application/pdf"]] + (t/is (some? (v3/validate-storage-object (assoc base :content-type ctype))) + (str "expected valid for " (pr-str ctype)))) + (doseq [ctype ["" " " "image /svg" "image/apng" "image/avif" + "application/x-font-woff" (apply str (repeat 200 "x"))]] + (t/is (thrown? clojure.lang.ExceptionInfo + (v3/validate-storage-object (assoc base :content-type ctype))) + (str "expected rejection for " (pr-str ctype))))))) + +(defn- write-temp-svg + "Write `text` to a tempfile and return `[path size]`." + [text] + (let [path (fs/create-tempfile :prefix "penpot-svg-import-test-" :suffix ".svg") + data (utf8bytes text)] + (spit (str path) text :encoding "UTF-8") + [path (alength data)])) + +(defn- tamper-svg-entry! + "Copy `src` zip to `dst` zip replacing the `objects/.svg` + payload with `evil-bytes` and fixing its `.json` sidecar (size and + blake2b hash) so the bundle passes the integrity checks. When + `content-type` is given, the sidecar declares it instead of the + original spelling." + [src dst storage-id ^bytes evil-bytes & {:keys [content-type]}] + (let [svg-name (str "objects/" storage-id ".svg") + json-name (str "objects/" storage-id ".json")] + (with-open [zin (ZipFile. (.toFile ^java.nio.file.Path src)) + zout (ZipOutputStream. (io/output-stream dst))] + (doseq [entry (enumeration-seq (.entries zin))] + (let [name (.getName ^ZipEntry entry)] + (cond + (= name svg-name) + (do (.putNextEntry zout (ZipEntry. ^String name)) + (.write zout evil-bytes 0 (alength evil-bytes)) + (.closeEntry zout)) + + (= name json-name) + (let [raw (slurp (.getInputStream zin entry) :encoding "UTF-8") + parsed (json/read-str raw) + updated (cond-> (assoc parsed + "size" (alength evil-bytes) + "hash" (sto/calculate-hash (ByteArrayInputStream. evil-bytes))) + content-type (assoc "contentType" content-type))] + (.putNextEntry zout (ZipEntry. ^String name)) + (.write zout (utf8bytes (json/write-str updated))) + (.closeEntry zout)) + + :else + (do (.putNextEntry zout (ZipEntry. ^String name)) + (with-open [in (.getInputStream zin entry)] + (io/copy in zout)) + (.closeEntry zout)))))))) + +(defn- setup-file-with-svg-media + "Upload `clean-svg`, attach it to a shape fill and return + `[file uploaded]` ready to export." + [profile n] + (let [file (th/create-file* n {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + [svg-path svg-size] (write-temp-svg clean-svg) + out (th/command! {::th/type :upload-file-media-object + ::rpc/profile-id (:id profile) + :file-id (:id file) + :is-local true + :name "benign.svg" + :content {:filename "benign.svg" + :path svg-path + :mtype "image/svg+xml" + :size svg-size}}) + _ (t/is (nil? (:error out)) (str "upload failed: " (pr-str (:error out)))) + uploaded (:result out) + page-id (first (get-in file [:data :pages])) + shape-id (uuid/random) + upd (th/command! {::th/type :update-file + ::rpc/profile-id (:id profile) + :id (:id file) + :session-id (uuid/random) + :revn 0 + :vern 0 + :features cfeat/supported-features + :changes + [{:type :add-obj + :page-id page-id + :id shape-id + :parent-id uuid/zero + :frame-id uuid/zero + :components-v2 true + :obj (cts/setup-shape + {:id shape-id + :name "image" + :frame-id uuid/zero + :parent-id uuid/zero + :type :rect + :fills [{:fill-opacity 1 + :fill-image {:id (:id uploaded) + :width (:width uploaded) + :height (:height uploaded) + :mtype "image/svg+xml"}}]})}]})] + (t/is (nil? (:error upd)) (str "update-file failed: " (pr-str (:error upd)))) + (t/is (uuid? (:media-id uploaded))) + [file uploaded])) + +(defn- export-tamper-import-v3 + "Export `file` to v3, tamper its SVG payload with `evil-text` + (declaring `content-type` when given) and import the bundle. + Returns `[served-bytes stored-content-type]` of the imported + media." + [profile file media-id evil-text content-type] + (let [bundle (tmp/tempfile :prefix "penpot-export-" :suffix ".zip")] + (v3/export-files! + (-> th/*system* + (assoc ::bfc/ids #{(:id file)}) + (assoc ::bfc/embed-assets false) + (assoc ::bfc/include-libraries false)) + (io/output-stream bundle)) + + (let [evil (tmp/tempfile :prefix "penpot-evil-" :suffix ".zip") + _ (tamper-svg-entry! bundle evil media-id (utf8bytes evil-text) :content-type content-type) + result (-> th/*system* + (assoc ::bfc/project-id (:default-project-id profile)) + (assoc ::bfc/profile-id (:id profile)) + (assoc ::bfc/input evil) + (v3/import-files!))] + (t/is (= 1 (count result))) + (let [rows (th/db-query :file-media-object {:file-id (first result)}) + storage (:app.storage/storage th/*system*)] + (t/is (= 1 (count rows))) + (let [object (sto/get-object storage (:media-id (first rows)))] + [(bytes-str (sto/get-object-bytes storage object)) + (:content-type (meta object))]))))) + +(t/deftest import-binfile-v3-sanitizes-smuggled-svg + (let [profile (th/create-profile* 1) + [file uploaded] (setup-file-with-svg-media profile 1) + [served _stored] (export-tamper-import-v3 profile file (:media-id uploaded) evil-svg nil)] + (t/is (not (str/includes? served " (sto/content raw) + (sto/wrap-with-hash (sto/calculate-hash (ByteArrayInputStream. raw)))) + sobj (sto/put-object! storage {::sto/content content + :content-type content-type + :bucket "file-media-object"}) + fmo-id (uuid/random) + _ (th/db-insert! :file-media-object + {:id fmo-id + :file-id (:id file) + :is-local true + :name "seed.svg" + :media-id (:id sobj) + :width 64 + :height 64 + :mtype content-type}) + page-id (first (get-in file [:data :pages])) + shape-id (uuid/random) + upd (th/command! {::th/type :update-file + ::rpc/profile-id (:id profile) + :id (:id file) + :session-id (uuid/random) + :revn 0 + :vern 0 + :features cfeat/supported-features + :changes + [{:type :add-obj + :page-id page-id + :id shape-id + :parent-id uuid/zero + :frame-id uuid/zero + :components-v2 true + :obj (cts/setup-shape + {:id shape-id + :name "image" + :frame-id uuid/zero + :parent-id uuid/zero + :type :rect + :fills [{:fill-opacity 1 + :fill-image {:id fmo-id + :width 64 + :height 64 + :mtype "image/svg+xml"}}]})}]})] + (t/is (nil? (:error upd)) (str "update-file failed: " (pr-str (:error upd)))) + [file (:id sobj)])) + +(defn- export-v1 + "Export `file` to v1 and return the bundle path." + [profile file] + (let [bundle (tmp/tempfile :prefix "penpot-export-v1-" :suffix ".bin")] + (v1/export-files! + (-> th/*system* + (assoc ::bfc/ids #{(:id file)}) + (assoc ::bfc/embed-assets false) + (assoc ::bfc/include-libraries false)) + (io/output-stream bundle)) + bundle)) + +(defn- export-import-v1 + "Export `file` to v1, import it back and return the served bytes + of its single media object." + [profile file] + (let [bundle (export-v1 profile file)] + (let [result (-> th/*system* + (assoc ::bfc/project-id (:default-project-id profile)) + (assoc ::bfc/profile-id (:id profile)) + (assoc ::bfc/input (io/input-stream bundle)) + (v1/import-files!))] + (t/is (= 1 (count result))) + (let [rows (th/db-query :file-media-object {:file-id (first result)}) + storage (:app.storage/storage th/*system*)] + (t/is (= 1 (count rows))) + (let [object (sto/get-object storage (:media-id (first rows)))] + [(bytes-str (sto/get-object-bytes storage object)) + (:content-type (meta object))]))))) + +(t/deftest import-binfile-v1-sanitizes-seeded-svg + (let [profile (th/create-profile* 4) + [file _seed] (seed-file-with-svg-media profile 4 evil-svg "IMAGE/SVG+XML") + [served stored] (export-import-v1 profile file)] + (t/is (not (str/includes? served " th/*system* + (assoc ::bfc/project-id (:default-project-id profile)) + (assoc ::bfc/profile-id (:id profile)) + (assoc ::bfc/input (io/input-stream bundle)) + (v1/import-files!)) + nil + (catch clojure.lang.ExceptionInfo e + (ex-data e)))] + (t/is (= :validation (:type out))) + (t/is (= :media-type-not-allowed (:code out))) + (t/is (= [(:id file)] + (mapv :id (th/db-query :file {:project-id (:default-project-id profile)})))))) + +(def ^:private large-clean-svg + (let [pad (apply str (repeat 110000 ""))] + (str "" pad ""))) + +(t/deftest import-binfile-v1-roundtrips-large-svg + (t/testing "objects over the tempfile threshold (tempfile branch) import fine" + (t/is (> (alength (utf8bytes large-clean-svg)) bfc/temp-file-threshold)) + (let [profile (th/create-profile* 5) + [file _seed] (seed-file-with-svg-media profile 5 large-clean-svg "image/svg+xml") + [served _stored] (export-import-v1 profile file)] + (t/is (str/includes? served "circle"))))) diff --git a/common/src/app/common/media.cljc b/common/src/app/common/media.cljc index a5a74e6c75..b7ec9b5a71 100644 --- a/common/src/app/common/media.cljc +++ b/common/src/app/common/media.cljc @@ -25,6 +25,23 @@ (def tempfile-types (conj image-types "application/pdf" "application/zip")) +(def storage-object-types + "Every content-type the system stores in storage objects: uploaded + images and fonts, generated thumbnails, temporary files and the + `application/octet-stream` fallback used by file data and font + variants. Sorted for determinism. Every member must have a + producing code path; anticipated but unproduced types (such as + `image/apng`) stay out until some flow actually stores them. + Import boundaries normalize the stored string (lowercase, no + parameters) before checking membership, so legacy spelling + variants keep importing while unknown types are rejected." + (into (sorted-set) + (concat image-types + font-types + ["application/octet-stream" + "application/pdf" + "application/zip"]))) + (defn format->extension [format] (case format diff --git a/common/test/common_tests/media_test.cljc b/common/test/common_tests/media_test.cljc index c6916e3216..c0440331bc 100644 --- a/common/test/common_tests/media_test.cljc +++ b/common/test/common_tests/media_test.cljc @@ -9,6 +9,13 @@ [app.common.media :as media] [clojure.test :as t])) +(t/deftest test-storage-object-types + (t/testing "covers every image and font type the system stores" + (t/is (every? #(contains? media/storage-object-types %) media/image-types)) + (t/is (every? #(contains? media/storage-object-types %) media/font-types)) + (t/is (contains? media/storage-object-types "image/svg+xml")) + (t/is (contains? media/storage-object-types "application/octet-stream")))) + (t/deftest test-parse-font-weight (t/testing "matches weight tokens with proper boundaries" (t/is (= 700 (media/parse-font-weight "Roboto-Bold"))) From ff7a66f51938b2293260ea27a64e2c4e0833b5e3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 6 Oct 2026 14:25:50 +0200 Subject: [PATCH 024/145] :books: Update changelog --- CHANGES.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 56d1046320..c48637725a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # CHANGELOG +## 2.18.3 + +### :bug: Bugs fixed + +- Fix binfile import storing unsanitized SVG and missing obfuscated SVG content-types [#12104](https://github.com/penpot/penpot/issues/12104) (PR: [#12105](https://github.com/penpot/penpot/pull/12105)) +- Fix removed team members keeping file access after leaving the team [#12106](https://github.com/penpot/penpot/issues/12106) (PR: [#12097](https://github.com/penpot/penpot/pull/12097)) + ## 2.18.2 ### :bug: Bugs fixed From a61a4fceb15fde77ea09877b83c774e56314e300 Mon Sep 17 00:00:00 2001 From: "penpot-renovate[bot]" <338182062+penpot-renovate[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:08:49 +0000 Subject: [PATCH 025/145] :arrow_up: Update dependency pnpm to v12.8.1 - pnpm 12.8.1 (docker/images/Dockerfile.media-processor) - pnpm 12.8.1 (docker/images/Dockerfile.mcp) - pnpm 12.8.1 (docker/images/Dockerfile.exporter) - pnpm 12.8.1 (render-wasm/package.json) - pnpm 12.8.1 (plugins/package.json) - pnpm 12.8.1 (plugins/libs/plugins-styles/package.json) - pnpm 12.8.1 (plugins/libs/plugins-runtime/package.json) - pnpm 12.8.1 (plugins/libs/plugin-types/package.json) - pnpm 12.8.1 (plugins/apps/table-plugin/package.json) - pnpm 12.8.1 (plugins/apps/rename-layers-plugin/package.json) - pnpm 12.8.1 (plugins/apps/poc-tokens-plugin/package.json) - pnpm 12.8.1 (plugins/apps/poc-state-plugin/package.json) - pnpm 12.8.1 (plugins/apps/plugin-api-test-suite/package.json) - pnpm 12.8.1 (plugins/apps/lorem-ipsum-plugin/package.json) - pnpm 12.8.1 (plugins/apps/icons-plugin/package.json) - pnpm 12.8.1 (plugins/apps/example-styles/package.json) - pnpm 12.8.1 (plugins/apps/e2e/package.json) - pnpm 12.8.1 (plugins/apps/create-palette-plugin/package.json) - pnpm 12.8.1 (plugins/apps/contrast-plugin/package.json) - pnpm 12.8.1 (plugins/apps/composable-test-suite/package.json) - pnpm 12.8.1 (plugins/apps/colors-to-tokens-plugin/package.json) - pnpm 12.8.1 (package.json) - pnpm 12.8.1 (media-processor/package.json) - pnpm 12.8.1 (mcp/packages/server/package.json) - pnpm 12.8.1 (mcp/packages/plugin/package.json) - pnpm 12.8.1 (mcp/packages/common/package.json) - pnpm 12.8.1 (mcp/package.json) - pnpm 12.8.1 (library/package.json) - pnpm 12.8.1 (frontend/text-editor/package.json) - pnpm 12.8.1 (frontend/packages/ui/package.json) - pnpm 12.8.1 (frontend/packages/tokenscript/package.json) - pnpm 12.8.1 (frontend/packages/mousetrap/package.json) - pnpm 12.8.1 (frontend/packages/draft-js/package.json) - pnpm 12.8.1 (frontend/package.json) - pnpm 12.8.1 (exporter/package.json) - pnpm 12.8.1 (docs/package.json) - pnpm 12.8.1 (common/package.json) - pnpm 12.8.1 (backend/package.json) Signed-off-by: penpot-renovate[bot] <338182062+penpot-renovate[bot]@users.noreply.github.com> --- backend/package.json | 2 +- backend/pnpm-lock.yaml | 122 +++++++++--------- common/package.json | 2 +- common/pnpm-lock.yaml | 122 +++++++++--------- docker/images/Dockerfile.exporter | 6 +- docker/images/Dockerfile.mcp | 6 +- docker/images/Dockerfile.media-processor | 6 +- docs/package.json | 2 +- docs/pnpm-lock.yaml | 122 +++++++++--------- exporter/package.json | 2 +- exporter/pnpm-lock.yaml | 122 +++++++++--------- frontend/package.json | 2 +- frontend/packages/draft-js/package.json | 2 +- frontend/packages/mousetrap/package.json | 2 +- frontend/packages/tokenscript/package.json | 2 +- frontend/packages/ui/package.json | 2 +- frontend/pnpm-lock.yaml | 122 +++++++++--------- frontend/text-editor/package.json | 2 +- library/package.json | 2 +- library/pnpm-lock.yaml | 122 +++++++++--------- mcp/package.json | 2 +- mcp/packages/common/package.json | 2 +- mcp/packages/plugin/package.json | 2 +- mcp/packages/server/package.json | 2 +- mcp/pnpm-lock.yaml | 122 +++++++++--------- media-processor/package.json | 2 +- media-processor/pnpm-lock.yaml | 122 +++++++++--------- package.json | 2 +- .../apps/colors-to-tokens-plugin/package.json | 2 +- .../apps/composable-test-suite/package.json | 2 +- plugins/apps/contrast-plugin/package.json | 2 +- .../apps/create-palette-plugin/package.json | 2 +- plugins/apps/e2e/package.json | 2 +- plugins/apps/example-styles/package.json | 2 +- plugins/apps/icons-plugin/package.json | 2 +- plugins/apps/lorem-ipsum-plugin/package.json | 2 +- .../apps/plugin-api-test-suite/package.json | 2 +- plugins/apps/poc-state-plugin/package.json | 2 +- plugins/apps/poc-tokens-plugin/package.json | 2 +- .../apps/rename-layers-plugin/package.json | 2 +- plugins/apps/table-plugin/package.json | 2 +- plugins/libs/plugin-types/package.json | 2 +- plugins/libs/plugins-runtime/package.json | 2 +- plugins/libs/plugins-styles/package.json | 2 +- plugins/package.json | 2 +- plugins/pnpm-lock.yaml | 122 +++++++++--------- pnpm-lock.yaml | 122 +++++++++--------- render-wasm/package.json | 2 +- render-wasm/pnpm-lock.yaml | 122 +++++++++--------- 49 files changed, 715 insertions(+), 715 deletions(-) diff --git a/backend/package.json b/backend/package.json index 1e103493fe..06e280f11a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 18113d0170..61fb053a66 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' diff --git a/common/package.json b/common/package.json index d921c77202..faef4c3980 100644 --- a/common/package.json +++ b/common/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "type": "module", "repository": { "type": "git", diff --git a/common/pnpm-lock.yaml b/common/pnpm-lock.yaml index a19e1083c1..df3f0eecaf 100644 --- a/common/pnpm-lock.yaml +++ b/common/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 14eb31f211..8d41a57273 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -95,17 +95,17 @@ WORKDIR /opt/penpot/exporter # pnpm ships as a system binary (same tarball + SHA pin as # docker/devenv/Dockerfile); the generated ./setup expects it on PATH. # Corepack is gone from Node 25+, so nothing here may use it. -ARG PNPM_VERSION=12.6.0 +ARG PNPM_VERSION=12.8.1 RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ aarch64|arm64) \ PNPM_ARCH='arm64'; \ - PNPM_SHA256='973af2b3eb9509416cf889a7336705062c936407d5fda07298814e7532f5dea1'; \ + PNPM_SHA256='75c5e34d3164fe3d3549580b1b1b59c9dd16543e558dec722b686193cdb7dfa1'; \ ;; \ amd64|x86_64) \ PNPM_ARCH='x64'; \ - PNPM_SHA256='3f4c66f668d0e84219679982d095b30da68325e1fac9284e7f5dc1584bd3d13e'; \ + PNPM_SHA256='db3906881f63e41b655e3b97d473603dc26326c2470d0b1b8083689e1bcbd1e8'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ diff --git a/docker/images/Dockerfile.mcp b/docker/images/Dockerfile.mcp index f95a6869b5..d175718177 100644 --- a/docker/images/Dockerfile.mcp +++ b/docker/images/Dockerfile.mcp @@ -11,7 +11,7 @@ WORKDIR /opt/penpot/mcp # pnpm ships as a system binary (same tarball + SHA pin as # docker/devenv/Dockerfile); ./setup expects it on PATH. # Corepack is gone from Node 25+, so nothing here may use it. -ARG PNPM_VERSION=12.6.0 +ARG PNPM_VERSION=12.8.1 RUN set -eux; \ apt-get -qq update; \ apt-get -qqy --no-install-recommends install curl ca-certificates gzip; \ @@ -20,11 +20,11 @@ RUN set -eux; \ case "${ARCH}" in \ aarch64|arm64) \ PNPM_ARCH='arm64'; \ - PNPM_SHA256='973af2b3eb9509416cf889a7336705062c936407d5fda07298814e7532f5dea1'; \ + PNPM_SHA256='75c5e34d3164fe3d3549580b1b1b59c9dd16543e558dec722b686193cdb7dfa1'; \ ;; \ amd64|x86_64) \ PNPM_ARCH='x64'; \ - PNPM_SHA256='3f4c66f668d0e84219679982d095b30da68325e1fac9284e7f5dc1584bd3d13e'; \ + PNPM_SHA256='db3906881f63e41b655e3b97d473603dc26326c2470d0b1b8083689e1bcbd1e8'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ diff --git a/docker/images/Dockerfile.media-processor b/docker/images/Dockerfile.media-processor index 4b16df1ca0..20831a6798 100644 --- a/docker/images/Dockerfile.media-processor +++ b/docker/images/Dockerfile.media-processor @@ -58,7 +58,7 @@ WORKDIR /opt/penpot/media-processor # pnpm ships as a system binary (same tarball + SHA pin as # docker/devenv/Dockerfile); ./setup expects it on PATH. # Corepack is gone from Node 25+, so nothing here may use it. -ARG PNPM_VERSION=12.6.0 +ARG PNPM_VERSION=12.8.1 RUN set -eux; \ apt-get -qq update; \ apt-get -qqy --no-install-recommends install gzip; \ @@ -67,11 +67,11 @@ RUN set -eux; \ case "${ARCH}" in \ aarch64|arm64) \ PNPM_ARCH='arm64'; \ - PNPM_SHA256='973af2b3eb9509416cf889a7336705062c936407d5fda07298814e7532f5dea1'; \ + PNPM_SHA256='75c5e34d3164fe3d3549580b1b1b59c9dd16543e558dec722b686193cdb7dfa1'; \ ;; \ amd64|x86_64) \ PNPM_ARCH='x64'; \ - PNPM_SHA256='3f4c66f668d0e84219679982d095b30da68325e1fac9284e7f5dc1584bd3d13e'; \ + PNPM_SHA256='db3906881f63e41b655e3b97d473603dc26326c2470d0b1b8083689e1bcbd1e8'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ diff --git a/docs/package.json b/docs/package.json index fb43d456ef..ef98f9bda3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -39,5 +39,5 @@ "markdown-it-anchor": "^10.0.0", "markdown-it-plantuml": "^1.4.1" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 191f3ace31..20554cfc04 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' diff --git a/exporter/package.json b/exporter/package.json index f596181f04..745c197685 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" diff --git a/exporter/pnpm-lock.yaml b/exporter/pnpm-lock.yaml index 36db736d8d..0b7c6140e3 100644 --- a/exporter/pnpm-lock.yaml +++ b/exporter/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' diff --git a/frontend/package.json b/frontend/package.json index 7618a35d4c..c2e408beac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "browserslist": [ "defaults" ], diff --git a/frontend/packages/draft-js/package.json b/frontend/packages/draft-js/package.json index 6618f61074..be8840e69d 100644 --- a/frontend/packages/draft-js/package.json +++ b/frontend/packages/draft-js/package.json @@ -4,7 +4,7 @@ "description": "Penpot Draft-JS Wrapper", "main": "index.js", "type": "module", - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "author": "Andrey Antukh", "license": "MPL-2.0", "dependencies": { diff --git a/frontend/packages/mousetrap/package.json b/frontend/packages/mousetrap/package.json index 64bbd16dad..28ca48e04a 100644 --- a/frontend/packages/mousetrap/package.json +++ b/frontend/packages/mousetrap/package.json @@ -4,7 +4,7 @@ "description": "Simple library for handling keyboard shortcuts", "main": "index.js", "type": "module", - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "author": "Craig Campbell", "license": "Apache-2.0 WITH LLVM-exception" } diff --git a/frontend/packages/tokenscript/package.json b/frontend/packages/tokenscript/package.json index 565edc0855..8cead2bb50 100644 --- a/frontend/packages/tokenscript/package.json +++ b/frontend/packages/tokenscript/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "type": "module", - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "author": "Andrey Antukh", "license": "MPL-2.0", "dependencies": { diff --git a/frontend/packages/ui/package.json b/frontend/packages/ui/package.json index d5b9e71100..bf0a6272df 100644 --- a/frontend/packages/ui/package.json +++ b/frontend/packages/ui/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "types": "./dist/index.d.ts", "type": "module", - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "exports": { ".": { "import": "./dist/index.js" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5bc040bee7..a27cf5821c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' diff --git a/frontend/text-editor/package.json b/frontend/text-editor/package.json index f47cdd851f..7b0b75daa9 100644 --- a/frontend/text-editor/package.json +++ b/frontend/text-editor/package.json @@ -28,5 +28,5 @@ "vite": "^8.2.0", "vitest": "^5.0.2" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/library/package.json b/library/package.json index 70d3675175..a9d582dcdb 100644 --- a/library/package.json +++ b/library/package.json @@ -3,7 +3,7 @@ "version": "1.2.0-RC1", "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "type": "module", "repository": { "type": "git", diff --git a/library/pnpm-lock.yaml b/library/pnpm-lock.yaml index ec675f85ca..769fda13d3 100644 --- a/library/pnpm-lock.yaml +++ b/library/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' diff --git a/mcp/package.json b/mcp/package.json index 98505ca3e5..980f26ce29 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -23,7 +23,7 @@ "type": "git", "url": "https://github.com/penpot/penpot.git" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "devDependencies": { "concurrently": "^10.0.5", "prettier": "^3.9.9" diff --git a/mcp/packages/common/package.json b/mcp/packages/common/package.json index c8bdde4bd0..e91b9ea873 100644 --- a/mcp/packages/common/package.json +++ b/mcp/packages/common/package.json @@ -4,7 +4,7 @@ "description": "Shared type definitions and interfaces for Penpot MCP", "main": "dist/index.js", "types": "dist/index.d.ts", - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "scripts": { "build": "tsc --build --clean && tsc --build", "watch": "tsc --watch", diff --git a/mcp/packages/plugin/package.json b/mcp/packages/plugin/package.json index f4b5e9d68c..1a44059515 100644 --- a/mcp/packages/plugin/package.json +++ b/mcp/packages/plugin/package.json @@ -3,7 +3,7 @@ "private": true, "version": "1.0.0", "type": "module", - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "scripts": { "start": "vite build --watch --config vite.config.ts", "start:multi-user": "pnpm run start", diff --git a/mcp/packages/server/package.json b/mcp/packages/server/package.json index e9eaa9cc75..f2561193d2 100644 --- a/mcp/packages/server/package.json +++ b/mcp/packages/server/package.json @@ -27,7 +27,7 @@ "engines": { "node": ">=20" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "dependencies": { "@modelcontextprotocol/node": "^2.1.0", "@modelcontextprotocol/server": "^2.1.0", diff --git a/mcp/pnpm-lock.yaml b/mcp/pnpm-lock.yaml index 00545675bb..1ebfccbe6d 100644 --- a/mcp/pnpm-lock.yaml +++ b/mcp/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' diff --git a/media-processor/package.json b/media-processor/package.json index 155498f016..ee5114c7b9 100644 --- a/media-processor/package.json +++ b/media-processor/package.json @@ -14,7 +14,7 @@ "fmt:check": "prettier --check src/ test/", "clean": "rm -rf dist/" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "dependencies": { "express": "^5.2.1", "multer": "^2.2.0", diff --git a/media-processor/pnpm-lock.yaml b/media-processor/pnpm-lock.yaml index 774b041d11..926fab2cd0 100644 --- a/media-processor/pnpm-lock.yaml +++ b/media-processor/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' diff --git a/package.json b/package.json index 1289dce20b..5f6e9cbec0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" diff --git a/plugins/apps/colors-to-tokens-plugin/package.json b/plugins/apps/colors-to-tokens-plugin/package.json index d7f66f0b8e..e26ce1c6ca 100644 --- a/plugins/apps/colors-to-tokens-plugin/package.json +++ b/plugins/apps/colors-to-tokens-plugin/package.json @@ -13,5 +13,5 @@ "lint": "eslint .", "test": "vitest" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/composable-test-suite/package.json b/plugins/apps/composable-test-suite/package.json index 20aae69b6c..b50b96386f 100644 --- a/plugins/apps/composable-test-suite/package.json +++ b/plugins/apps/composable-test-suite/package.json @@ -27,5 +27,5 @@ "vite": "^8.2.2", "vite-live-preview": "^0.4.0" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/contrast-plugin/package.json b/plugins/apps/contrast-plugin/package.json index 341e530bc4..aa177c08d3 100644 --- a/plugins/apps/contrast-plugin/package.json +++ b/plugins/apps/contrast-plugin/package.json @@ -13,5 +13,5 @@ "lint": "eslint .", "test": "vitest" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/create-palette-plugin/package.json b/plugins/apps/create-palette-plugin/package.json index 6c2ed36828..961a360e9f 100644 --- a/plugins/apps/create-palette-plugin/package.json +++ b/plugins/apps/create-palette-plugin/package.json @@ -12,5 +12,5 @@ "lint": "eslint .", "test": "vitest" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/e2e/package.json b/plugins/apps/e2e/package.json index 5f837261ee..c3e0d53630 100644 --- a/plugins/apps/e2e/package.json +++ b/plugins/apps/e2e/package.json @@ -7,5 +7,5 @@ "test": "vitest", "lint": "eslint ." }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/example-styles/package.json b/plugins/apps/example-styles/package.json index 1f05146cdf..73f65ac488 100644 --- a/plugins/apps/example-styles/package.json +++ b/plugins/apps/example-styles/package.json @@ -11,5 +11,5 @@ "serve": "vite preview", "lint": "eslint ." }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/icons-plugin/package.json b/plugins/apps/icons-plugin/package.json index a290d2afd0..2bf7416462 100644 --- a/plugins/apps/icons-plugin/package.json +++ b/plugins/apps/icons-plugin/package.json @@ -13,5 +13,5 @@ "lint": "eslint .", "test": "vitest" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/lorem-ipsum-plugin/package.json b/plugins/apps/lorem-ipsum-plugin/package.json index 6e874c26f8..56989beb20 100644 --- a/plugins/apps/lorem-ipsum-plugin/package.json +++ b/plugins/apps/lorem-ipsum-plugin/package.json @@ -13,5 +13,5 @@ "lint": "eslint .", "test": "vitest" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/plugin-api-test-suite/package.json b/plugins/apps/plugin-api-test-suite/package.json index 28f81cac97..38b786484c 100644 --- a/plugins/apps/plugin-api-test-suite/package.json +++ b/plugins/apps/plugin-api-test-suite/package.json @@ -20,5 +20,5 @@ "devDependencies": { "playwright": "^1.62.1" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/poc-state-plugin/package.json b/plugins/apps/poc-state-plugin/package.json index 2c563bd33a..6cd5d0cf94 100644 --- a/plugins/apps/poc-state-plugin/package.json +++ b/plugins/apps/poc-state-plugin/package.json @@ -13,5 +13,5 @@ "lint": "eslint .", "test": "vitest" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/poc-tokens-plugin/package.json b/plugins/apps/poc-tokens-plugin/package.json index 61654d0646..dd52a6e2d3 100644 --- a/plugins/apps/poc-tokens-plugin/package.json +++ b/plugins/apps/poc-tokens-plugin/package.json @@ -13,5 +13,5 @@ "lint": "eslint .", "test": "exit 0" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/rename-layers-plugin/package.json b/plugins/apps/rename-layers-plugin/package.json index 179d878ab5..2ac3b42cbc 100644 --- a/plugins/apps/rename-layers-plugin/package.json +++ b/plugins/apps/rename-layers-plugin/package.json @@ -13,5 +13,5 @@ "lint": "eslint .", "test": "vitest" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/apps/table-plugin/package.json b/plugins/apps/table-plugin/package.json index 6ab4bd4881..3fc0a470ef 100644 --- a/plugins/apps/table-plugin/package.json +++ b/plugins/apps/table-plugin/package.json @@ -13,5 +13,5 @@ "lint": "eslint .", "test": "vitest" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/libs/plugin-types/package.json b/plugins/libs/plugin-types/package.json index 2fac5e0feb..a836bdd829 100644 --- a/plugins/libs/plugin-types/package.json +++ b/plugins/libs/plugin-types/package.json @@ -7,5 +7,5 @@ "build": "node ../../tools/scripts/build-types.mjs", "lint": "tsc -p . --noEmit" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/libs/plugins-runtime/package.json b/plugins/libs/plugins-runtime/package.json index c752df719c..891d97fbaf 100644 --- a/plugins/libs/plugins-runtime/package.json +++ b/plugins/libs/plugins-runtime/package.json @@ -16,5 +16,5 @@ "lint": "eslint .", "test": "vitest" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/libs/plugins-styles/package.json b/plugins/libs/plugins-styles/package.json index 26d4ee0d1d..6b84336dc3 100644 --- a/plugins/libs/plugins-styles/package.json +++ b/plugins/libs/plugins-styles/package.json @@ -6,5 +6,5 @@ "build": "node ../../tools/scripts/build-css.mjs", "lint": "echo 0" }, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45" } diff --git a/plugins/package.json b/plugins/package.json index 60354ab878..399d026962 100644 --- a/plugins/package.json +++ b/plugins/package.json @@ -3,7 +3,7 @@ "version": "0.6.0", "type": "module", "license": "MIT", - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "scripts": { "start": "pnpm run start:app:runtime", "start:app:runtime": "concurrently --kill-others --names build,server \"pnpm --filter @penpot/plugins-runtime run build:watch\" \"pnpm --filter @penpot/plugins-runtime run preview\"", diff --git a/plugins/pnpm-lock.yaml b/plugins/pnpm-lock.yaml index 470b8c5bba..09b47524ea 100644 --- a/plugins/pnpm-lock.yaml +++ b/plugins/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e2bdb7d80..58cb8b5a61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' diff --git a/render-wasm/package.json b/render-wasm/package.json index 795a79d0c7..0529bfc873 100644 --- a/render-wasm/package.json +++ b/render-wasm/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC Sucursal en España SL", "private": true, - "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", + "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" diff --git a/render-wasm/pnpm-lock.yaml b/render-wasm/pnpm-lock.yaml index b8a64120ea..79c219f770 100644 --- a/render-wasm/pnpm-lock.yaml +++ b/render-wasm/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.6.0 - version: 12.6.0 + specifier: 12.8.1 + version: 12.8.1 packages: - '@pnpm/exe.android-arm64@12.6.0': - resolution: {integrity: sha512-kviIHft9h02q+7N2In7tSL9T/HRUSGwYmU74YPtsWU5ee7vgqPyC0JT6RRX6miW+NxX1CvHgQxg69qq1qTLJYQ==} + '@pnpm/exe.android-arm64@12.8.1': + resolution: {integrity: sha512-siDxcidfz5eM6L2tJfik+i2bbUEw8UC7MZ3Yl2HYdRmqbdhHGAOQACtOOu6QVW4Jul4/w5wVKm+wM8ACtgh1Pg==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.6.0': - resolution: {integrity: sha512-CT8aJKLq2mtZFE71pr4E5Z2xHL8uGnRGr+NclDqsXlVF4SVcQ2QAs14mWi0C8gjT4hbmi+Q0lJRRIkMAWfjjng==} + '@pnpm/exe.android-x64@12.8.1': + resolution: {integrity: sha512-0F/uqPRc3aN4vLHsa0f+6fTtmsigFQ9/yxEU43fionF//pNHEd5e97b3w/nKIgqYMCUQCJphmBRMA9Q8/wlo9Q==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.6.0': - resolution: {integrity: sha512-rafpVkjzugKBMxSvGQd5wK1x5eOTkD/+lcJIFHeQhvd+BJ8kDisOOvwhQDrpGd4vd2Fx+hhW0P2Ptt638OlhFg==} + '@pnpm/exe.darwin-arm64@12.8.1': + resolution: {integrity: sha512-/rwavvMQJsl2RIxOHBnDF+4Gk+fhQFAY4M3PQZoKskJOEnaJcygG9p1xby6wcQcbS4d9dIubv09CQhufTgfmpw==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.6.0': - resolution: {integrity: sha512-72Jpuv1m24gI8zUNcaylYpBWEhBzz1VPhnDIU2GYM2eRP+KsOqVLwV78i+JrihI39zY2NNXmaN4eAKX1inpaDQ==} + '@pnpm/exe.darwin-x64@12.8.1': + resolution: {integrity: sha512-htYt2gN7zqJKLhC99Ft6EUiO7myjZAZXfmGCTJUL1v+o7vE9hZdE1+fAkk4Oj1ZHNRti6b2woOxEkRkAusi8OQ==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.6.0': - resolution: {integrity: sha512-LON4QgNy1w/XF4uySZIyC/5hEB0oGdelXsY9tSKqstER+2j2X2oHz67skD11RRd/HWFRtMr64shQZ4I9ZoZ0zg==} + '@pnpm/exe.freebsd-x64@12.8.1': + resolution: {integrity: sha512-PB5BULNgytxvExoOXUUeseS/DcpPENs+cg3iA6xHHeaLT4ctEGKSGXEbuj7USeqvo8JBpAx039hivnbzGqDwnA==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.6.0': - resolution: {integrity: sha512-BdDpX+DeaMUc5x6WNcE6FkmFy36NwrBbBvwsZU737Zt3u1fTY+iKFPchjEQNCM4rT6q6xvF2oN3ZwExGsJCkxQ==} + '@pnpm/exe.linux-arm64-musl@12.8.1': + resolution: {integrity: sha512-kKeyC/ArjrgdciQRBrOL0j9HtOI4gDRgoyP+eJ1CQKyadpKlf/DhDDyspXe07R3B9KagTVLUF2nlt6Dr5quFIg==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.6.0': - resolution: {integrity: sha512-8h2sNoIhHDpHDYqZZCh9PXr6krqW10btb0GHVGuw710muDHlFAHMtUG2ogmaHge6zJG1xgY4Y7mwX5g/LWvrYg==} + '@pnpm/exe.linux-arm64@12.8.1': + resolution: {integrity: sha512-q4s9a9X2O3N9aeUFooW24orNrxvu20+jyVUFR5RanPCqOKKPBrgs2iywMkBBx1aXkkzdWT54/mfz8Be8sJlWEw==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.6.0': - resolution: {integrity: sha512-ld6xgcEhFCFrsVsJhbmdmR714PnsEspO/Ij8w0/D7OSiz2hYGu3fZd5tKRxOI5m6H6Gl0/RRFAPncTHbsgXVjQ==} + '@pnpm/exe.linux-ppc64@12.8.1': + resolution: {integrity: sha512-F7XSjKJthwCh/L6abZiMpAfo2Cm61SZIT3e4dfgiIjESndlWaXCnfLKngSgZ/SEMsdmHT6dud3+kWIB3tw0/xQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.6.0': - resolution: {integrity: sha512-HIbImydoC3J8NFt+8MXfhTCf5rO6NjCMyinSAHac0VmV8uCHw+kbQkyAzxV84rzz9ZOzmBOnj0b1N461UIYGsw==} + '@pnpm/exe.linux-riscv64@12.8.1': + resolution: {integrity: sha512-z1ecpIK/ZY08Unk69KWWE9867oaKUytGUjrHnBoGFAAhsK+q+0cfvBH+f5fWobJ7FX97leMlLO+gkJolddM45Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.6.0': - resolution: {integrity: sha512-DjSqT7+BZ/lWcDHwNMuzsRVfyxUimh+D54Xw4tSWbdVVQhtWtNMUyI93LVnwCmsoEpar2gC3SFeAgywx37svKQ==} + '@pnpm/exe.linux-s390x@12.8.1': + resolution: {integrity: sha512-KU6tnrBfu4uKXTq6N5qJwSm8FSrs9Wc8b5AUEsHK9bRXwNdsZ1/lRxG1++X29SKRW9OSNuDSjxASv5mAvGo1gA==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.6.0': - resolution: {integrity: sha512-31lKeGPmRE6xfV6I3VjaEGVFRei14pl/zgoFDK4w+UkbJqOuNl2Ht3O1GuqNIg3Gl14qTEb4kZRAYFHXifDx9A==} + '@pnpm/exe.linux-x64-musl@12.8.1': + resolution: {integrity: sha512-eUE8BWJkDr2tJb/yrk0yVnszlmNqFarVxUlqiZ9BAkct28Nq/5g2H0tvMDmPA7fAQW3STNj+omJPPRVT+4s5sQ==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.6.0': - resolution: {integrity: sha512-qFWBneHJAJ73W4whtbaFOL1M/7DBC6ILHXuxc7ZPtEhfPuT1zeZiGrmKHoMAfJA+mcm6xhOFljqVTUS+00Jabw==} + '@pnpm/exe.linux-x64@12.8.1': + resolution: {integrity: sha512-8bDZ0lZlCdi2rvnFul7EYgcC7zKeWSe76y8JV2oXobaS8p9i9d6PSf6LgLtTM0fI7R9Oh4eLCbveXyNDMmvZ+g==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.6.0': - resolution: {integrity: sha512-OhfefXEEykZlslSUhR8PPRe6MVPV3Oink/dVfIecE5velpc7j80cZYsHlaDrtRDcm3TPwQHkaRW1SJ1efX9oFg==} + '@pnpm/exe.win32-arm64@12.8.1': + resolution: {integrity: sha512-MsT0dlrRxnCRCHCaosNbhWpEiAFnFuMEu3KuDJDU41BXgTwiINxU8goloUSUDejWZUqKaUqNC/qUTihl9nFtqg==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.6.0': - resolution: {integrity: sha512-L2tuyrD2+Imgxs3VK/ST/2L3xD6vDP0ktJQxM0TaGw7TbAQEMq+RMc8iK6Ew7Id579uzjJe8jSfJID2ZGsOHCA==} + '@pnpm/exe.win32-x64@12.8.1': + resolution: {integrity: sha512-SWtPe0PsbTTk//gJND10tMdOfCYjrkq5+qV49hzVhY7xz2iJ339Rc+xwASlcvbwTfrvH39YNqpKYibD+CRGLwA==} cpu: [x64] os: [win32] - pnpm@12.6.0: - resolution: {integrity: sha512-PvaPlRyxEawgS0paFvCy3fDaVqluBBPoHYVdnwtV75JnFHCQKOHNAMQFwsX7e56OxNxGd3yAXQNzwvL/AP0g7A==} + pnpm@12.8.1: + resolution: {integrity: sha512-9kupB1B/XOr+BsjTjmBS0BeURFgOwSed3Vv8EctIqoomRLZlmOB+dh2oSHKp/FfV+QK4f6SdAkGY1VhhKqu+RQ==} engines: {node: '>=18.*'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.6.0': + '@pnpm/exe.android-arm64@12.8.1': optional: true - '@pnpm/exe.android-x64@12.6.0': + '@pnpm/exe.android-x64@12.8.1': optional: true - '@pnpm/exe.darwin-arm64@12.6.0': + '@pnpm/exe.darwin-arm64@12.8.1': optional: true - '@pnpm/exe.darwin-x64@12.6.0': + '@pnpm/exe.darwin-x64@12.8.1': optional: true - '@pnpm/exe.freebsd-x64@12.6.0': + '@pnpm/exe.freebsd-x64@12.8.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.6.0': + '@pnpm/exe.linux-arm64-musl@12.8.1': optional: true - '@pnpm/exe.linux-arm64@12.6.0': + '@pnpm/exe.linux-arm64@12.8.1': optional: true - '@pnpm/exe.linux-ppc64@12.6.0': + '@pnpm/exe.linux-ppc64@12.8.1': optional: true - '@pnpm/exe.linux-riscv64@12.6.0': + '@pnpm/exe.linux-riscv64@12.8.1': optional: true - '@pnpm/exe.linux-s390x@12.6.0': + '@pnpm/exe.linux-s390x@12.8.1': optional: true - '@pnpm/exe.linux-x64-musl@12.6.0': + '@pnpm/exe.linux-x64-musl@12.8.1': optional: true - '@pnpm/exe.linux-x64@12.6.0': + '@pnpm/exe.linux-x64@12.8.1': optional: true - '@pnpm/exe.win32-arm64@12.6.0': + '@pnpm/exe.win32-arm64@12.8.1': optional: true - '@pnpm/exe.win32-x64@12.6.0': + '@pnpm/exe.win32-x64@12.8.1': optional: true - pnpm@12.6.0: + pnpm@12.8.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.6.0 - '@pnpm/exe.android-x64': 12.6.0 - '@pnpm/exe.darwin-arm64': 12.6.0 - '@pnpm/exe.darwin-x64': 12.6.0 - '@pnpm/exe.freebsd-x64': 12.6.0 - '@pnpm/exe.linux-arm64': 12.6.0 - '@pnpm/exe.linux-arm64-musl': 12.6.0 - '@pnpm/exe.linux-ppc64': 12.6.0 - '@pnpm/exe.linux-riscv64': 12.6.0 - '@pnpm/exe.linux-s390x': 12.6.0 - '@pnpm/exe.linux-x64': 12.6.0 - '@pnpm/exe.linux-x64-musl': 12.6.0 - '@pnpm/exe.win32-arm64': 12.6.0 - '@pnpm/exe.win32-x64': 12.6.0 + '@pnpm/exe.android-arm64': 12.8.1 + '@pnpm/exe.android-x64': 12.8.1 + '@pnpm/exe.darwin-arm64': 12.8.1 + '@pnpm/exe.darwin-x64': 12.8.1 + '@pnpm/exe.freebsd-x64': 12.8.1 + '@pnpm/exe.linux-arm64': 12.8.1 + '@pnpm/exe.linux-arm64-musl': 12.8.1 + '@pnpm/exe.linux-ppc64': 12.8.1 + '@pnpm/exe.linux-riscv64': 12.8.1 + '@pnpm/exe.linux-s390x': 12.8.1 + '@pnpm/exe.linux-x64': 12.8.1 + '@pnpm/exe.linux-x64-musl': 12.8.1 + '@pnpm/exe.win32-arm64': 12.8.1 + '@pnpm/exe.win32-x64': 12.8.1 --- lockfileVersion: '9.0' From 5981d1ee66a73d199385490bf2c0bf7625d59033 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 6 Oct 2026 15:40:24 +0200 Subject: [PATCH 026/145] :bug: Open a never scrolled token set at the top of the list (#12119) The tokens list container stays mounted when another token set is selected; only the restore key changes. With no saved position for the new set, use-restore-scroll did nothing, so the list kept the scroll position of the previous set. Scroll to the top when nothing is saved for the key. Freshly mounted panels are already at the top, so Layers and Assets do not change. Closes #11973 AI-assisted-by: claude-opus-5-5 --- .../app/main/ui/workspace/sidebar/scroll.cljs | 15 ++++++++++++--- .../ui/sidebar_scroll_test.cljs | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs b/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs index 7bcaa73bef..235e29dec5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/scroll.cljs @@ -45,17 +45,26 @@ (when (needs-restore-retry? prev-height height attempts) (vreset! raf* (tm/raf #(restore-loop! node saved height (inc attempts) raf*)))))) +(defn- restore-position! + "Restores saved, or scrolls to the top when there is none: the node can + stay mounted while the key changes (token sets), so it may still hold + another list's position." + [node saved raf*] + (if (some? saved) + (restore-loop! node saved -1 0 raf*) + (restore-scroll! node 0))) + (defn use-restore-scroll "Restores the scrollTop saved under [panel id] on mount and whenever panel or id change. Retries while the content height keeps changing so deep positions in lazily rendered lists are not clamped to the first - chunk. Missing key or node is a silent no-op." + chunk. With no saved position the node goes to the top. Missing node is + a silent no-op." [store* panel id node-ref] (mf/with-effect [panel id] (let [raf* (volatile! nil)] (when-let [node (mf/ref-val node-ref)] - (when-let [saved (get @store* [panel id])] - (restore-loop! node saved -1 0 raf*))) + (restore-position! node (get @store* [panel id]) raf*)) (fn [] (when-some [raf @raf*] (tm/cancel-af! raf)))))) diff --git a/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs b/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs index 9cf7bd8bcc..609ed6c35b 100644 --- a/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs +++ b/frontend/test/frontend_tests/ui/sidebar_scroll_test.cljs @@ -11,6 +11,7 @@ [cljs.test :as t :include-macros true])) (def ^:private restore-loop! @#'sc/restore-loop!) +(def ^:private restore-position! @#'sc/restore-position!) (defn- scroll-event [scroll-top] @@ -64,3 +65,21 @@ ((first @scheduled)) (t/is (= 250 (.-scrollTop node))) (t/is (= 1 (count @scheduled)) "no further frames once settled")))) + +(t/deftest restore-position-without-saved-value-goes-to-top + (let [node #js {:scrollTop 400 :scrollHeight 900} + scheduled (atom []) + raf* (volatile! nil)] + (with-redefs [tm/raf (fn [f] (swap! scheduled conj f) 1)] + (restore-position! node nil raf*) + (t/is (= 0 (.-scrollTop node)) "a node kept mounted drops the old position") + (t/is (empty? @scheduled) "no retry loop for the top")))) + +(t/deftest restore-position-with-saved-value-runs-loop + (let [node #js {:scrollTop 0 :scrollHeight 900} + scheduled (atom []) + raf* (volatile! nil)] + (with-redefs [tm/raf (fn [f] (swap! scheduled conj f) 1)] + (restore-position! node 250 raf*) + (t/is (= 250 (.-scrollTop node))) + (t/is (= 1 (count @scheduled)) "first pass schedules a settle check")))) From 0ccd75487fb8e26eda30162b7b14224e89023c12 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 6 Oct 2026 15:43:15 +0200 Subject: [PATCH 027/145] :arrow_up: Update dependencies on devenv --- docker/devenv/Dockerfile | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 3d841a45be..07fbbcf2de 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -67,7 +67,7 @@ RUN set -ex; \ FROM base AS setup-node ENV NODE_VERSION=v24.21.0 \ - PNPM_VERSION=12.8.1 \ + PNPM_VERSION=12.9.1 \ PATH=/opt/node/bin:$PATH RUN set -eux; \ @@ -77,13 +77,13 @@ RUN set -eux; \ OPENSSL_ARCH='linux-aarch64'; \ BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \ PNPM_ARCH='arm64'; \ - PNPM_SHA256='75c5e34d3164fe3d3549580b1b1b59c9dd16543e558dec722b686193cdb7dfa1'; \ + PNPM_SHA256='468294b77633d29889c6e4a79cefb1277eba30cd62e7d6a754218cb85a1d730d'; \ ;; \ amd64|x86_64) \ OPENSSL_ARCH='linux-x86_64'; \ BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \ PNPM_ARCH='x64'; \ - PNPM_SHA256='db3906881f63e41b655e3b97d473603dc26326c2470d0b1b8083689e1bcbd1e8'; \ + PNPM_SHA256='b4f58449ac02d9d24023ac89e17e05e8331f8ddc12ca1f489cf231bc2bb2bce1'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ @@ -109,18 +109,18 @@ RUN set -eux; \ FROM base AS setup-opencode -ENV OPENCODE_VERSION=2.0.21 +ENV OPENCODE_VERSION=2.0.24 RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ aarch64|arm64) \ BINARY_URL="https://registry.npmjs.org/@opencode/cli-linux-arm64/-/cli-linux-arm64-${OPENCODE_VERSION}.tgz"; \ - ESUM='046c0e0e8eec8f6085d46c0eca29ece65fb00e21df0881c81cbcf7392458b1d2'; \ + ESUM='9d0cd2bfc060fd6c2afdf6db69837f8690d46a8d8327bb864421851ce004f453'; \ ;; \ amd64|x86_64) \ BINARY_URL="https://registry.npmjs.org/@opencode/cli-linux-x64/-/cli-linux-x64-${OPENCODE_VERSION}.tgz"; \ - ESUM='57db421ab689d6831178c84736ba1e667c9c27627866849a114fa57afbc81785'; \ + ESUM='21b1ee0683841405d69541fc44481f8e5752e95bea72e8b8ec31dbcdb102e7f8'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; exit 1; \ @@ -140,7 +140,7 @@ RUN set -eux; \ FROM base AS setup-caddy -ENV CADDY_VERSION=2.11.4 +ENV CADDY_VERSION=2.11.7 RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ @@ -255,12 +255,12 @@ RUN set -eux; \ FROM base AS setup-utils ENV CLJKONDO_VERSION=2026.08.04 \ - BABASHKA_VERSION=1.13.223 \ - CLJFMT_VERSION=0.16.5 \ + BABASHKA_VERSION=1.13.225 \ + CLJFMT_VERSION=0.16.6 \ PIXI_VERSION=0.81.0 \ - GITHUB_CLI_VERSION=2.101.0 \ - AWS_CLI_VERSION=2.36.49 \ - UV_VERSION=0.12.17 \ + GITHUB_CLI_VERSION=2.102.0 \ + AWS_CLI_VERSION=2.37.9 \ + UV_VERSION=0.12.23 \ UV_TOOL_DIR=/opt/uv/tools \ UV_TOOL_BIN_DIR=/opt/utils/bin \ UV_PYTHON_INSTALL_DIR=/opt/uv/python \ @@ -375,11 +375,11 @@ RUN set -ex; \ case "${ARCH}" in \ aarch64|arm64) \ BINARY_ARCH="aarch64"; \ - BINARY_SHA256="2dec26fdaff2f5e34961ea98f456297104f73d8409fc3599c8f2909c6e27a07e"; \ + BINARY_SHA256="e7d2cca3622af4765871fe9ce7eeed03acd09dca10a99ebc46e92f79d78765c0"; \ ;; \ amd64|x86_64) \ BINARY_ARCH="x86_64"; \ - BINARY_SHA256="6b51b62d232217350e72d8f2493f47d3461239d011d1d5eef960dfcd8cb07069"; \ + BINARY_SHA256="6b3a6a3d7bb3997928f0bdf7b866914224abf242c2e54e1dcebbe84bee64f356"; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ From f38cb4dc07c188df617fb385ba6fa96e46b28937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 6 Oct 2026 15:32:49 +0200 Subject: [PATCH 028/145] :fire: Remove unused package-lock.json from plugins-runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Barragán Merino --- .../libs/plugins-runtime/package-lock.json | 43 ------------------- 1 file changed, 43 deletions(-) delete mode 100644 plugins/libs/plugins-runtime/package-lock.json diff --git a/plugins/libs/plugins-runtime/package-lock.json b/plugins/libs/plugins-runtime/package-lock.json deleted file mode 100644 index b484be67cd..0000000000 --- a/plugins/libs/plugins-runtime/package-lock.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@penpot/plugins-runtime", - "version": "0.12.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@penpot/plugins-runtime", - "version": "0.12.0", - "dependencies": { - "@penpot/plugin-types": "^0.12.0", - "ses": "^1.1.0", - "zod": "^3.22.4" - } - }, - "node_modules/@endo/env-options": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@endo/env-options/-/env-options-1.1.6.tgz", - "integrity": "sha512-Uqy94PwLTco90Yfign43muvDtjsYTbL6Ck4W5sSWQUqJiFOL+YP8kiBOoGyQynDsGTY0MCQvMxEPfphaJKnzlQ==" - }, - "node_modules/@penpot/plugin-types": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@penpot/plugin-types/-/plugin-types-0.12.0.tgz", - "integrity": "sha512-rtSJ8memBAWxRBEx+S47NFDJN5VIvY0J7QzwTBer45Ls3BjLCaWXCnfFEcRn4SzY+Th01tmop0pwdvN5YDxkBw==" - }, - "node_modules/ses": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ses/-/ses-1.8.0.tgz", - "integrity": "sha512-pis9agyAy7s9lDApaYY9OSktkzc10gkBggSbhHXwTf3cWL9H8xA7oKiXIfT/uVNf8dzZN3o2TtPYjer9/axIhw==", - "dependencies": { - "@endo/env-options": "^1.1.6" - } - }, - "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} From fcbb8f1fc093b46ab3df9bb4a4d06c602731600a Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 6 Oct 2026 16:34:28 +0200 Subject: [PATCH 029/145] :bug: Keep derived layout paddings non-negative (#12129) Adding a flex or grid layout to a board with children derives the paddings from the distance between the board and its children. A child that overflows the board, or floating point noise after "Resize board to fit content", gave negative paddings. The shape schema rejects them since paddings must be non-negative, so update-file failed and autosave stopped. Clamp the derived paddings to 0, as the derived gaps already are. Closes #12107 AI-assisted-by: claude-opus-5-5 --- .../geom/shapes/flex_layout/params.cljc | 4 +-- .../geom/shapes/grid_layout/params.cljc | 4 +-- .../common_tests/geom_flex_layout_test.cljc | 31 +++++++++++++++++++ .../common_tests/geom_grid_layout_test.cljc | 25 +++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/common/src/app/common/geom/shapes/flex_layout/params.cljc b/common/src/app/common/geom/shapes/flex_layout/params.cljc index 57a1fab63d..0d7d67ba1b 100644 --- a/common/src/app/common/geom/shapes/flex_layout/params.cljc +++ b/common/src/app/common/geom/shapes/flex_layout/params.cljc @@ -88,8 +88,8 @@ parent-selrect (:selrect parent) padding (when (and (not (nil? parent)) (> (count shapes) 0)) - {:p1 (- min-y (:y1 parent-selrect)) - :p2 (- min-x (:x1 parent-selrect))})] + {:p1 (max 0 (- min-y (:y1 parent-selrect))) + :p2 (max 0 (- min-x (:x1 parent-selrect)))})] (cond-> {:layout-flex-dir direction :layout-gap layout-gap} (not (nil? padding)) diff --git a/common/src/app/common/geom/shapes/grid_layout/params.cljc b/common/src/app/common/geom/shapes/grid_layout/params.cljc index ee3a779942..e310dfadd1 100644 --- a/common/src/app/common/geom/shapes/grid_layout/params.cljc +++ b/common/src/app/common/geom/shapes/grid_layout/params.cljc @@ -154,8 +154,8 @@ layout-grid-columns (mapv (constantly ctl/default-track-value) cols) parent-childs-vector (gpt/to-vec (gpo/origin (:points parent)) (gpt/point all-shapes-rect)) - p-left (:x parent-childs-vector) - p-top (:y parent-childs-vector)] + p-left (max 0 (:x parent-childs-vector)) + p-top (max 0 (:y parent-childs-vector))] (-> {:layout-grid-columns layout-grid-columns :layout-grid-rows layout-grid-rows diff --git a/common/test/common_tests/geom_flex_layout_test.cljc b/common/test/common_tests/geom_flex_layout_test.cljc index c016daeea7..4e6f6a149c 100644 --- a/common/test/common_tests/geom_flex_layout_test.cljc +++ b/common/test/common_tests/geom_flex_layout_test.cljc @@ -7,6 +7,7 @@ (ns common-tests.geom-flex-layout-test (:require [app.common.geom.rect :as grc] + [app.common.geom.shapes.flex-layout.params :as flparams] [app.common.geom.shapes.flex-layout.positions :as flp] [app.common.math :as mth] [app.common.types.shape :as cts] @@ -104,3 +105,33 @@ :layout-justify-content :end :x 0 :y 0 :width 100 :height 100})] (t/is (not (ctl/v-end? frame)))))) + +;; ---- calculate-params paddings ---- +;; +;; Adding a flex layout derives the paddings from the distance between the +;; board and its children. A child that overflows the board, or floating +;; point noise after "Resize board to fit content", gave negative paddings +;; that the shape schema rejects, which broke autosave (#12107). + +(defn- params-for-child + [child-attrs] + (let [parent (cts/setup-shape {:type :frame :x 0 :y 0 :width 100 :height 100}) + child (cts/setup-shape (merge {:type :rect :parent-id (:id parent)} child-attrs)) + parent (assoc parent :shapes [(:id child)]) + objects {(:id parent) parent (:id child) child}] + (flparams/calculate-params objects [child] parent))) + +(t/deftest calculate-params-keeps-positive-padding + (t/is (= {:p1 20 :p2 10 :p3 20 :p4 10} + (:layout-padding (params-for-child {:x 10 :y 20 :width 50 :height 50}))))) + +(t/deftest calculate-params-clamps-overflowing-child-padding + (t/is (= {:p1 0 :p2 0 :p3 0 :p4 0} + (:layout-padding (params-for-child {:x -10 :y -5 :width 150 :height 150}))))) + +(t/deftest calculate-params-clamps-float-noise-padding + (let [padding (:layout-padding + (params-for-child {:x -1.1728368917829357E-5 + :y -2.1457672119140625E-6 + :width 100 :height 100}))] + (t/is (every? #(not (neg? %)) (vals padding))))) diff --git a/common/test/common_tests/geom_grid_layout_test.cljc b/common/test/common_tests/geom_grid_layout_test.cljc index 1769ab634a..ac642b2d43 100644 --- a/common/test/common_tests/geom_grid_layout_test.cljc +++ b/common/test/common_tests/geom_grid_layout_test.cljc @@ -11,6 +11,7 @@ [app.common.geom.modifiers] [app.common.geom.rect :as grc] [app.common.geom.shapes.grid-layout.layout-data :as gld] + [app.common.geom.shapes.grid-layout.params :as glparams] [app.common.math :as mth] [app.common.types.shape :as cts] [clojure.test :as t])) @@ -408,3 +409,27 @@ result (gld/set-auto-multi-span (auto-col-parent) tracks cmap cells frame-bounds {} :column)] (t/is (mth/close? 100.0 (:size (nth result 0)) 0.001)) (t/is (mth/close? 100.0 (:size (nth result 1)) 0.001))))) + +;; --------------------------------------------------------------------------- +;; calculate-params: derived paddings are never negative +;; --------------------------------------------------------------------------- +;; +;; Adding a grid layout derives the paddings from the children. A child that +;; overflows the board gave negative paddings that the shape schema rejects, +;; which broke autosave (#12107). + +(defn- grid-params-for-children + [& children-attrs] + (let [parent (cts/setup-shape {:type :frame :x 0 :y 0 :width 100 :height 100}) + children (mapv #(cts/setup-shape (merge {:type :rect :parent-id (:id parent)} %)) + children-attrs)] + (glparams/calculate-params nil children parent))) + +(t/deftest grid-calculate-params-keeps-positive-padding + (let [params (grid-params-for-children {:x 10 :y 20 :width 30 :height 30})] + (t/is (mth/close? 20 (get-in params [:layout-padding :p1]))) + (t/is (mth/close? 10 (get-in params [:layout-padding :p2]))))) + +(t/deftest grid-calculate-params-clamps-overflowing-child-padding + (let [params (grid-params-for-children {:x -10 :y -5 :width 150 :height 150})] + (t/is (= {:p1 0 :p2 0 :p3 0 :p4 0} (:layout-padding params))))) From d046f6997d0bca494b5e74c542badc1b498ee889 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 6 Oct 2026 16:55:16 +0200 Subject: [PATCH 030/145] :bug: Keep assets list view scroll position across sidebar tabs (#12115) In list view the height of each component row depended on its thumbnail, which renders deferred. When the Assets tab mounted again, rows were shorter until the thumbnails appeared, so the restored scrollTop landed several rows further down and scroll anchoring kept it there. Reserve the thumbnail height in the row grid so rows keep their size from the first render. Closes #11971 AI-assisted-by: claude-opus-5-5 --- .../src/app/main/ui/workspace/sidebar/assets/components.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss index c09e1e4d0f..c3d442b4c0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss @@ -69,6 +69,10 @@ .component-item-list { display: grid; grid-template-columns: auto 1fr auto; + + // The thumbnail renders deferred; reserve its height so rows keep their + // size on mount and the saved sidebar scroll lands on the same item. + grid-template-rows: $sz-40; align-items: center; column-gap: var(--sp-s); padding: var(--sp-xxs); From 472973533e0c889eb926f92db06f5d281fc71aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 6 Oct 2026 14:34:59 +0000 Subject: [PATCH 031/145] :whale: Verify the Node.js tarball checksum in the devenv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Barragán Merino --- docker/devenv/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 619e826549..995284048a 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -76,12 +76,14 @@ RUN set -eux; \ aarch64|arm64) \ OPENSSL_ARCH='linux-aarch64'; \ BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \ + NODE_SHA256='724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5'; \ PNPM_ARCH='arm64'; \ PNPM_SHA256='468294b77633d29889c6e4a79cefb1277eba30cd62e7d6a754218cb85a1d730d'; \ ;; \ amd64|x86_64) \ OPENSSL_ARCH='linux-x86_64'; \ BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \ + NODE_SHA256='6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff'; \ PNPM_ARCH='x64'; \ PNPM_SHA256='b4f58449ac02d9d24023ac89e17e05e8331f8ddc12ca1f489cf231bc2bb2bce1'; \ ;; \ @@ -91,6 +93,7 @@ RUN set -eux; \ ;; \ esac; \ curl -LfsSo /tmp/nodejs.tar.gz ${BINARY_URL}; \ + echo "${NODE_SHA256} */tmp/nodejs.tar.gz" | sha256sum -c -; \ mkdir -p /opt/node; \ cd /opt/node; \ tar -xf /tmp/nodejs.tar.gz --strip-components=1; \ From 7a37cefe4cbd78c4292398fcec361316dfc55f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 6 Oct 2026 17:33:43 +0200 Subject: [PATCH 032/145] :bug: Pass team-id in binfile SVG import tests (#12139) AI-assisted-by: claude-opus-5-5 --- backend/test/backend_tests/binfile_svg_test.clj | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/test/backend_tests/binfile_svg_test.clj b/backend/test/backend_tests/binfile_svg_test.clj index 500c497df1..41d8d212ad 100644 --- a/backend/test/backend_tests/binfile_svg_test.clj +++ b/backend/test/backend_tests/binfile_svg_test.clj @@ -255,6 +255,7 @@ _ (tamper-svg-entry! bundle evil media-id (utf8bytes evil-text) :content-type content-type) result (-> th/*system* (assoc ::bfc/project-id (:default-project-id profile)) + (assoc ::bfc/team-id (:default-team-id profile)) (assoc ::bfc/profile-id (:id profile)) (assoc ::bfc/input evil) (v3/import-files!))] From b6a3ab67efc917ce3b9199d87c774c22222259b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 6 Oct 2026 18:00:24 +0200 Subject: [PATCH 033/145] :fire: Remove Dependabot configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Barragán Merino --- .github/dependabot.yml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 80e71fedab..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,20 +0,0 @@ -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file -version: 2 -updates: - # Actions are pinned to a commit SHA with a `# vX.Y.Z` comment; Dependabot - # bumps both. All updates are grouped into a single monthly PR. - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "team infra" - - "no issue required" - commit-message: - prefix: ":arrow_up:" - groups: - github-actions: - patterns: - - "*" From 8204e97a98aef628137e8eb29349d61ffab5b538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 6 Oct 2026 15:31:27 +0000 Subject: [PATCH 034/145] :wrench: Manage GitHub Actions with Renovate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Barragán Merino --- .github/workflows/build-renovate.yml | 6 +++++- renovate.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-renovate.yml b/.github/workflows/build-renovate.yml index bb7b9d9b40..6156aed086 100644 --- a/.github/workflows/build-renovate.yml +++ b/.github/workflows/build-renovate.yml @@ -28,6 +28,7 @@ jobs: contents: read outputs: short_sha: ${{ steps.bundle.outputs.short_sha }} + images: ${{ steps.changes.outputs.images }} devenv: ${{ steps.changes.outputs.devenv }} imagemagick: ${{ steps.changes.outputs.imagemagick }} @@ -49,9 +50,11 @@ jobs: BASE_SHA: ${{ steps.base.outputs.sha }} run: | changed() { - git diff --quiet "$BASE_SHA" HEAD -- "$1" && echo false || echo true + git diff --quiet "$BASE_SHA" HEAD -- "$@" && echo false || echo true } { + # Anything outside .github can affect the production images. + echo "images=$(changed . ':!.github')" echo "devenv=$(changed docker/devenv)" echo "imagemagick=$(changed docker/imagemagick)" } >> "$GITHUB_OUTPUT" @@ -85,6 +88,7 @@ jobs: runs-on: penpot-standar-runner timeout-minutes: 60 needs: prepare + if: needs.prepare.outputs.images == 'true' permissions: contents: read strategy: diff --git a/renovate.json b/renovate.json index bfe3f97ff5..1d7acb30de 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,7 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["github>penpot/renovate-config"], + "labels": ["dependencies", "team infra", "no issue required"], "semanticCommits": "disabled", "commitMessagePrefix": ":arrow_up:", "commitMessageAction": "Update", From 1afa8b1afa8e47211d84c0ab43b35b3e5a980891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 6 Oct 2026 16:02:06 +0000 Subject: [PATCH 035/145] :wrench: Skip triage for Renovate's dashboard and GitHub Actions PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Barragán Merino --- .github/workflows/auto-label.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index e3c435e40d..84615efe4b 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -13,8 +13,14 @@ permissions: {} jobs: triage: - # Dependabot PRs are labelled by dependabot.yml and do not need triage. - if: github.actor != 'dependabot[bot]' + # The Renovate Dependency Dashboard and Renovate's GitHub Actions PRs do + # not need triage. Other Renovate PRs (base images, Node.js, pnpm) do. + if: >- + !(github.event_name == 'issues' + && github.event.issue.user.login == 'penpot-renovate[bot]') + && !(github.event_name == 'pull_request_target' + && github.event.pull_request.user.login == 'penpot-renovate[bot]' + && github.event.pull_request.head.ref == 'renovate-github-actions') runs-on: ubuntu-latest steps: - name: Generate GitHub App token From 4bdded86be49f5093d5a8cafce1400cc9d9080b5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 6 Oct 2026 16:26:37 +0000 Subject: [PATCH 036/145] :recycle: Extract changelog checks into scripts/changelog.py Move the inline programs out of the update-changelog skill into scripts/changelog.py (check-merged, cross-ref, report) and shrink the skill to the workflow alone. Teach scripts/gh.py to retry transient HTTP 504s, resolve lookups in batches of 50 (new issue subcommand, milestone on prs output), and report :boom: entries without the breaking change label. Add the nine missing 2.17.0 changelog entries found while trying the new flow. AI-assisted-by: muse-spark-1.3 --- .agents/skills/update-changelog/SKILL.md | 934 ++++------------------- .serena/memories/critical-info.md | 1 + .serena/memories/scripts/changelog.md | 18 + .serena/memories/scripts/gh.md | 11 +- CHANGES.md | 9 + scripts/changelog.py | 616 +++++++++++++++ scripts/gh.py | 273 ++++++- 7 files changed, 1036 insertions(+), 826 deletions(-) create mode 100644 .serena/memories/scripts/changelog.md create mode 100644 scripts/changelog.py diff --git a/.agents/skills/update-changelog/SKILL.md b/.agents/skills/update-changelog/SKILL.md index 5fa2883d6f..bd279754bc 100644 --- a/.agents/skills/update-changelog/SKILL.md +++ b/.agents/skills/update-changelog/SKILL.md @@ -20,7 +20,8 @@ primary link, with the fix PR inline on the same line. - `gh` CLI authenticated (`gh auth status`) - Python 3.8+ -- `scripts/gh.py` helper script available +- `scripts/gh.py` for GitHub queries, `scripts/changelog.py` for changelog + checks (run `python3 scripts/changelog.py --help` for usage) ## Workflow @@ -31,9 +32,6 @@ if not specified. ### 2. Fetch all issues in the milestone -Use the helper script. It uses GraphQL for efficient single-pass fetching -(closing PRs are included in the same query — no N+1): - ```bash # All closed issues (default) python3 scripts/gh.py issues "2.16.0" @@ -46,214 +44,135 @@ python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" ``` **Exclusion rules (issue-level):** -- `no changelog` label — Chore/refactor work that doesn't need a changelog entry -- `release blocker` label — Blocked issues not yet ready for changelog -- `Task` issue type — Internal chores are not user-facing; automatically excluded by `gh.py`. Use `--include-tasks` to override. -- **Rejected project status** — Issues with a "Rejected" status in the "Main" project board are automatically excluded by `gh.py`. This project-level status (independent of the GitHub issue `state`) indicates the issue was rejected from the release. Use `--include-rejected` to override. +- `no changelog` label — chore/refactor work, no entry needed +- `release blocker` label — blocked issues not yet ready for changelog +- `Task` issue type — internal chores, not user-facing; excluded by `gh.py` + (use `--include-tasks` to override) +- **Rejected project status** — issues with "Rejected" status on the "Main" + project board are excluded by `gh.py` (use `--include-rejected` to override). + This status is independent of the GitHub issue `state`. -**Exclusion rules (PR-level):** -In addition to issue-level exclusions, PRs with these labels should be -excluded regardless of their linked issue's labels: -- `release blocker` — PR is part of a pending release blocker batch -- `no issue required` — Trivial fix not tracked as an issue +**Exclusion rules (PR-level):** PRs with these labels stay out regardless of +their linked issue's labels: `release blocker`, `no issue required`. -The script outputs JSON with each entry containing `number`, `title`, `state`, -`issue_type`, `labels`, `closing_prs` (the PRs that fix each issue), and -`project_status` (the "Main" project board status, e.g. "Done", "Rejected", -or `null` if not tracked in a project). +Each entry carries `number`, `title`, `state`, `issue_type`, `labels`, +`closing_prs`, and `project_status`. ### 3. Identify missing entries (optional) -If updating from an existing `CHANGES.md`, find issues in the milestone that -are NOT yet referenced in the changelog: - ```bash python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" --compare CHANGES.md ``` -This returns a filtered JSON array with only the missing issues. - -> **Note:** The `--compare` flag checks **issues** only (via issue number -> references in the changelog). To find merged **PRs** not yet referenced, -> use the milestone PR cross-reference described in step 10 below. +Returns only milestone issues not yet referenced in the changelog. Note: it +compares **issues** only. For unreferenced merged **PRs**, use the +cross-reference in step 8. ### 4. Fetch additional PR details when needed -When you need more context for specific PRs (e.g. to find the PR author for -community contribution attribution, or to read the PR body for -"Fixes/Closes #NNN" patterns): - ```bash -# One or more PR numbers +# One or more PR numbers (also: --file prs.txt, or --stdin) python3 scripts/gh.py prs 9179 9204 9311 -# From a file -python3 scripts/gh.py prs --file prs.txt - -# From stdin -cat prs.txt | python3 scripts/gh.py prs --stdin -``` - -The `prs` command also supports listing all PRs in a milestone in one call: - -```bash -# All merged PRs in a milestone (default) +# All merged PRs in a milestone (default); --state all/open/closed for others python3 scripts/gh.py prs --milestone "2.16.0" - -# All states (merged, open, closed) -python3 scripts/gh.py prs --milestone "2.16.0" --state all ``` -The `prs` command returns JSON with `number`, `title`, `body`, `state`, -`merged_at`, `author`, `labels`, and `closing_issues`. PRs are fetched in -batches of 50 via GraphQL to stay within API limits (milestone mode uses -paginated GraphQL on the milestone's `pullRequests` connection). - -You can also list all PRs in a milestone in a single call: - -```bash -# All merged PRs in a milestone (default) -python3 scripts/gh.py prs --milestone "2.16.0" - -# All states (merged, open, closed) -python3 scripts/gh.py prs --milestone "2.16.0" --state all - -# Open PRs only -python3 scripts/gh.py prs --milestone "2.16.0" --state open -``` - -The milestone path uses paginated GraphQL on the milestone's `pullRequests` -connection (100 per page), avoiding one-by-one fetches. +Returns `number`, `title`, `body`, `state`, `merged_at`, `author`, `labels`, +and `closing_issues`. Milestone mode uses paginated GraphQL (100 per page). ### 5. Categorize entries — strictly by issue type, never by labels or emoji -Use the **Issue Type** field (GitHub's native issue type, exposed as -`issue_type` in the `gh.py` JSON output) to determine which section an entry -belongs to. +Use the **Issue Type** field (`issue_type` in the `gh.py` output). No separate +query is needed. > **⚠️ CRITICAL: Never use labels or title emoji prefixes for categorization.** -> Labels like `bug` and `enhancement`, as well as title prefixes like `:bug:` -> and `:sparkles:`, are frequently inaccurate, missing, or contradictory to the -> actual issue type. The `issue_type` field from `gh.py` is the single source -> of truth. +> Labels like `bug`/`enhancement` and prefixes like `:bug:`/`:sparkles:` are +> often wrong or missing. `issue_type` is the single source of truth. | `issue_type` value | Changelog section | |--------------------|-------------------| | `Bug` | `### :bug: Bugs fixed` | | `Feature` or `Enhancement` | `### :sparkles: New features & Enhancements` | -| `Task` | **Exclude** — internal chores are not user-facing | -| `null` (not set) | Check labels as a fallback: `bug` label → bugs, otherwise enhancements | +| `Task` | **Exclude** — internal chores, not user-facing | +| `null` (not set) | Fallback to labels: `bug` label → bugs, otherwise enhancements | -The `gh.py` issues command already includes `issue_type` in every entry's -output. **No separate GraphQL query is needed.** +**Breaking changes override everything:** an issue with the `breaking change` +label goes under `### :boom: Breaking changes & Deprecations`, no matter its +issue type. This is the only label-based rule — it is an explicit exception +to the "never by labels" principle above. The `:boom:` subsection goes first +in the version, before `:rocket:` (matching the existing precedent). -**Preserve highlighted entries:** If an entry is already featured in -`### :rocket: Epics and highlights`, keep it in that section when refreshing a -changelog version. Do not remove a highlighted entry just because issue type -categorization would otherwise place it under `### :sparkles: New features & -Enhancements`. +**Preserve highlighted entries:** if an entry already sits under +`### :rocket: Epics and highlights`, keep it there when refreshing. Do not +move it down just because its type would place it under `:sparkles:`. -**Community contribution attribution:** If the issue or its fix PR has the -`community contribution` label, add an attribution `(by @)` -on the changelog entry line, **before** the GitHub issue/PR references. +**Community attribution:** if the issue or its fix PR has the +`community contribution` label, add `(by @)` on the entry +line, **before** the issue/PR references. Use the **PR author** (the `author` +field from step 4), not the issue author: -The attribution should reference the **PR author**, not the issue author. -The `prs` subcommand includes the `author` field — use that: - -```bash -python3 scripts/gh.py prs | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])" -``` - -Placement in the entry line: ```markdown - Fix description of the bug (by @username) [#](...) (PR: [#](...)) ``` -**Only closed issues are included.** An issue must have `state: "closed"` to -appear in the changelog. Open/unresolved issues are omitted, even if they are -tracked in the milestone. +**Only closed issues are included**, even if open ones are tracked in the +milestone. **Pairing rules:** | Pattern | Changelog format | |---------|-----------------| -| Closed issue + one or more PRs fix it | Primary link = issue, PR inline comma-separated | -| PR exists with no linked issue | If a corresponding closed issue exists in the same milestone, link the issue. Otherwise, skip the entry (the issue must be the changelog unit). | -| Closed issue with no fix PR in milestone | Link the issue directly, without a PR reference. | +| Closed issue + one or more fix PRs | Primary link = issue, PRs inline comma-separated | +| PR with no linked issue | Link the issue if a matching closed one exists in the milestone; otherwise skip (the issue is the changelog unit) | +| Closed issue with no fix PR in milestone | Link the issue directly, no PR reference | -> **False-positive associations:** A PR may incorrectly claim to close an issue -> from a different context (e.g., a very old PR referencing a modern issue, or a -> cross-project reference). If the PR title and issue title are clearly unrelated, -> or the PR was created years before the issue, treat it as a data glitch and -> skip it. PR [#3](https://github.com/penpot/penpot/pull/3) (ancient License PR -> claiming to close a plugin API issue) is a known example. +> **False-positive associations:** a PR may wrongly claim to close an issue +> from another context (ancient PR, cross-project reference). If titles are +> clearly unrelated or the PR predates the issue by years, treat it as a data +> glitch and skip it. -### 5a. ⚠️ Verify PR merge status before writing +### 5a. Verify PR merge status before writing A closed issue may list closing PRs that were **closed without merging** -(e.g., a community PR that was superseded by another). The changelog must -only reference **merged** PRs. Verify before writing: +(e.g. a superseded community PR). Only **merged** PRs go in the changelog: ```bash -# Collect all PR numbers from the candidate entries and check them -python3 scripts/gh.py prs | python3 -c " -import json, sys -for pr in json.load(sys.stdin): - if pr['state'] != 'MERGED': - print(f'WARNING: #{pr[\"number\"]} is {pr[\"state\"]} (not merged)') -" +python3 scripts/changelog.py check-merged +# also accepts: --file prs.txt, or numbers via --stdin ``` -If a closing PR is closed-unmerged, find the actual merged PR that -superseded it: -1. Check the issue's closing PRs list for other PRs (there may be multiple) -2. Look for other PRs with similar titles or descriptions referencing the same issue -3. Inspect the closed PR's conversation timeline for a pointer to the replacement - -Replace the reference in the changelog entry with the correct merged PR number. +If a closing PR is closed-unmerged, find the merged PR that superseded it +(other PRs in the issue's closing list, similar titles, or pointers in the +closed PR's timeline) and reference that one instead. ### 5b. Security advisory (GHSA) entries -Security advisories fixed in a release are documented in the changelog even -though they are **neither milestone issues nor PRs**. The GHSA ID and its -description are supplied by the user or the release notes — they never come -from the milestone fetch in step 2. - -**Format** (matches the existing precedent in `CHANGES.md`, e.g. the -`create-font-variant` arbitrary file read advisory): +Advisories fixed in a release go in the changelog even though they are +**neither milestone issues nor PRs**. The GHSA ID and description come from +the user or the release notes — never from the milestone fetch. ```markdown - Fix (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX) ``` -Rules: -- Place the entry under `### :bug: Bugs fixed`, with **no issue or PR link** — - only the advisory URL. -- The advisory may be **draft/unpublished** at changelog time (the URL 404s - publicly). Do **not** web-fetch or verify the URL, and do **not** drop the - entry because of that. Rely on the GHSA ID provided by the user. -- Derive the description from the supplied advisory title, imperative mood and - user-facing (e.g. `Fix command injection in SVG exporter via legacy fill-color`). -- These entries are **invisible to the automation**: they are not returned by - `gh.py issues`, not matched by `--compare` (step 3), not part of the PR - cross-reference (step 10), and not scanned by the anomaly-report regexes - (step 11, which only match `issues/` and `pull/` links). Add them manually. -- During pre-flight checks (step 6a) apply only the **backport/duplicate** - check: if the same GHSA already appears in an earlier version section, remove - it from the current section. Their absence from milestone cross-references - is expected, not an anomaly. +Rules: place under `### :bug: Bugs fixed` with **no issue or PR link**; do +**not** fetch or verify the URL (it may be draft/unpublished and 404); write +the description in imperative mood from the advisory title. These entries are +invisible to the automation — add them by hand, and in step 9 apply only the +backport/duplicate check to them. -### 6. Read the current CHANGES.md +### 6. Read the current CHANGES.md and run pre-flight checks -Read the top of `CHANGES.md` to understand the existing format and find the -insertion point (newest version goes at the top, after the `# CHANGELOG` -header). - -Key format rules from the existing file: +Newest version goes at the top, right after the `# CHANGELOG` header: ```markdown ## +### :boom: Breaking changes & Deprecations + +- [#](https://github.com/penpot/penpot/issues/) (PR: [#](https://github.com/penpot/penpot/pull/)) + ### :bug: Bugs fixed - Fix description of the bug [#](https://github.com/penpot/penpot/issues/) (PR: [#](https://github.com/penpot/penpot/pull/)) @@ -264,86 +183,38 @@ Key format rules from the existing file: - Add new feature description [#](https://github.com/penpot/penpot/issues/) (PR: [#](https://github.com/penpot/penpot/pull/)) ``` -Format details: -- Entries start with `- ` followed by a short description in imperative mood -- Primary link is **always the issue** (user-facing artifact) -- PR references are inline on the same line: `(PR: [#]())` - If an issue has multiple fix PRs, they are comma-separated: - `(PR: [#](), [#]())` -- The description should describe the fix/feature from the user's perspective -- Community contributions get `(by @)` **before** the issue link -- Sections are separated by a blank line between the last entry and the next - section title -- Only include a section if there are entries for it -- When an entry already exists in an earlier version section, it must be removed - from the current version to avoid duplicates +Format details: entries start with `- ` plus a short imperative description; +PR refs stay inline (`(PR: [#]())`, comma-separated for several); +`(by @)` goes before the issue link; only include non-empty +sections; blank line between a section's last entry and the next title; never +duplicate an entry from an earlier version section (the earlier version wins +for backports). -### 6a. Pre-flight checks — fix rule violations in the changelog +**Pre-flight checks — fix violations directly in `CHANGES.md` before writing +the new section.** Reconcile every existing entry (any version section) and +every milestone candidate against the current milestone state (re-fetch, do +not trust cached data): -**The LLM must apply these checks during the workflow and fix any -violations directly in `CHANGES.md`. They are not anomalies — they are -process errors that should be corrected before writing the new section.** - -The changelog is a *snapshot* of the milestone at a point in time, but -milestones and changelog entries can drift. The LLM must reconcile the -existing changelog against the current state of the milestone and the -existing changelog entries. - -For each entry that already exists in `CHANGES.md` (in any version -section) or in the candidate set for the current milestone, check: - -1. **Duplicate across versions.** Is the same issue already documented - in another (older) version section? If yes, this is a *backport*: - - The user-facing fix was already released. Remove the duplicate - from the current section. The earlier version is the canonical - reference. - -2. **Stale milestone assignment.** Has the issue been moved out of the - current milestone since the changelog was last updated (e.g., a fix - arrived late and the issue was reassigned to a future milestone)? - - Verify the issue is still in the current milestone via - `python3 scripts/gh.py issues --state all`. If it's no - longer there, remove the entry from the current section. (If the - target section doesn't exist yet, the entry is simply dropped.) - -3. **Exclusion labels newly applied.** Did the issue acquire a - `no changelog` or `release blocker` label since the changelog was - last updated? If yes, remove the entry from the current section. - -4. **Issue state changed.** Is the issue still closed? Has it been - reopened, deleted, or moved to a `Rejected` project status? If yes, +1. **Duplicate across versions** → remove from the current section. +2. **Stale milestone assignment** (issue moved out of this milestone) → + remove the entry (or drop it if the section does not exist yet). +3. **Newly applied exclusion labels** (`no changelog`, `release blocker`) → remove the entry. - -5. **Unmerged or removed PR references.** For every PR referenced in - the entry, is the PR still merged? Was the PR closed without - merging (superseded)? Was the PR moved to a different milestone? - If the only referenced PR is no longer merged, fix the reference - (find the actual merged fix PR) or remove the entry. A PR that is - merged in a *different* milestone is reported as an anomaly in - step 11 — do not silently remove it. - -6. **Issue type changed.** Did the issue type change (e.g., from Bug to - Task)? If the new type is `Task`, the issue is internal and should - be removed. - -7. **Cross-section completeness.** For every closed, non-excluded - milestone issue that is *not* referenced in any version section of - the changelog, add it to the current section (per the categorization - rules in step 5). - -After these checks, the changelog should be internally consistent with -the milestone. **Do not defer these fixes to step 11 — they are -workflow errors, not anomalies.** Step 11 only reports milestone -mismatches that require human judgment about the team's release -intent. +4. **Issue no longer closed/deleted/Rejected** → remove the entry. +5. **Unmerged or moved PR reference** → fix the reference or remove the + entry (a PR merged in a *different* milestone is a step-9 anomaly, do not + silently remove it). +6. **Issue type changed to `Task`** → remove the entry. +7. **Breaking change misplaced** (issue has the `breaking change` label but + sits in another section) → move the entry to `:boom:`. +8. **Missing valid issues** (closed, non-excluded, unreferenced anywhere) → + add them to the current section per step 5. ### 7. Build the description text -Derive the description from the issue title, not the PR title. Strip leading -emoji prefixes (`:bug:`, `:sparkles:`, `:tada:`) and focus on the -user-facing behavior. - -Examples: +Derive it from the **issue title**, not the PR title. Strip leading emoji +prefixes (`:bug:`, `:sparkles:`, `:tada:`) and describe the user-facing +behavior: | Issue title | Changelog description | |-------------|----------------------| @@ -351,589 +222,72 @@ Examples: | `Comment content is not sanitized before rendering, enabling stored XSS` | `Sanitize comment content on rendering` | | `Custom uploaded font family names are not sanitized` | `Sanitize font family names on custom uploaded fonts` | -### 8. Insert the section into CHANGES.md +Insert the new version section right after the `# CHANGELOG` header with the +`edit` tool and enough context for a unique match. -Insert the new version section right after the `# CHANGELOG` header (before -the previous version entry). Use the `edit` tool with enough context to make -a unique match. +### 7b. Propose and populate `:rocket: Epics and highlights` -### 8b. Propose and populate the `:rocket: Epics and highlights` subsection +Create the subsection if missing (place it before `### :sparkles:`) and pick +2–5 of the most impactful/user-visible `:sparkles:` entries: new visible +features, big capabilities, items that make self-hosted users want to update. +`frontend/src/app/main/ui/releases/v2_.cljs` slide titles are optional +hints (they may not exist for every version). Every `:rocket:` entry MUST +carry issue AND PR references; never remove entries from a prior run. -After inserting the version section, proactively create or populate the -`### :rocket: Epics and highlights` subsection. This section surfaces the -most impactful changes for self-hosted users checking for updates. - -**When to create:** If the version section does not already have a -`### :rocket: Epics and highlights` subsection, create one. Place it before -`### :sparkles:` (matching existing order in CHANGES.md). - -**How to identify highlights:** Review the `:sparkles:` entries for the -version and select 2–5 of the most impactful/user-visible ones. Criteria: -- New user-visible features (not internal refactors) -- Significant capability additions -- Items that create "FOMO" for self-hosted users on older versions - -**Use release notes as hints:** Check -`frontend/src/app/main/ui/releases/v2_.cljs` for the corresponding -version. The slide titles and feature descriptions there are curated -marketing content indicating what the team considers highlight-worthy. Match -those themes to changelog entries. Treat these files as optional hints — they -may not exist for every version. - -**Format requirement:** Every `:rocket:` entry MUST follow the standard -changelog format with issue/PR references: -``` -- [#](https://github.com/penpot/penpot/issues/) (PR: [#](https://github.com/penpot/penpot/pull/)) -``` -An entry without issue AND PR references is a highlight gap (warning, not an anomaly). - -**Preserve existing entries:** If the `:rocket:` section already exists from -a prior run, preserve its entries. Do not remove or rewrite them. - -### 9. Verify - -Read the top of `CHANGES.md` and confirm: -- The version header is correct -- Every entry has a GitHub link -- Entries with a fix PR have the PR sub-line -- The section ordering is correct (newest first) -- Formatting matches the surrounding entries - -### 10. Cross-reference milestone PRs against the changelog - -Issues can be fixed by PRs that aren't in the milestone, and merged PRs in -the milestone may not close any tracked issue. After writing, run a full -cross-reference to catch gaps: +### 8. Cross-reference milestone PRs against the changelog ```bash -# List all merged PRs in the milestone -python3 scripts/gh.py prs --milestone "" --state merged > /tmp/milestone-prs.json - -# Extract PR numbers from the changelog section -python3 -c " -import json, re - -with open('CHANGES.md') as f: - content = f.read() - -# Extract the version section (adjust regex to match the actual version) -match = re.search(r'## \(Unreleased\)\n(.*?)(?:\n## |\Z)', content, re.DOTALL) -section = match.group(1) - -# Collect all PR numbers referenced -changelog_prs = set() -for m in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)', section): - changelog_prs.add(int(m)) - -# Collect all milestone PRs (filtered) -with open('/tmp/milestone-prs.json') as f: - milestone_prs = json.load(f) - -milestone_merged = {pr['number'] for pr in milestone_prs} - -# PRs in milestone but not in changelog -missing = sorted(milestone_merged - changelog_prs) -print(f'Milestone merged PRs: {len(milestone_merged)}') -print(f'Changelog referenced PRs: {len(changelog_prs)}') -print(f'PRs in milestone but NOT in changelog: {len(missing)}') -for num in missing: - pr = next(p for p in milestone_prs if p['number'] == num) - print(f' #{num} {pr[\"title\"][:80]}') -" +python3 scripts/changelog.py cross-ref "" [--changes CHANGES.md] ``` -For each missing PR found, decide whether it should be added to the -changelog or is legitimately excluded (check its labels). +Lists merged milestone PRs missing from the changelog section (decide per PR: +add it or confirm its exclusion labels) and warns about CLOSED (unmerged) PRs +in the milestone. GHSA entries never appear here — that absence is expected. -Also verify that no closed-unmerged PRs remain in the changelog: +### 9. Generate the anomaly report ```bash -python3 scripts/gh.py prs --milestone "" --state all | python3 -c " -import json, sys -data = json.load(sys.stdin) -closed = [p for p in data if p['state'] == 'CLOSED'] -if closed: - print('WARNING: CLOSED (unmerged) PRs in milestone:') - for p in closed: - print(f' #{p[\"number\"]} {p[\"title\"][:80]}') -" +python3 scripts/changelog.py report "" [--changes CHANGES.md --output CHANGES-ISSUES.md] ``` -**Post-edit audit checklist:** -- ✅ All referenced PRs are merged (no closed-unmerged artifacts) -- ✅ Every merged milestone PR is either in the changelog or excluded by label -- ✅ PR and issue counts are internally consistent -- ✅ No false-positive PR-to-issue associations -- ✅ Advisory (GHSA) entries are not milestone PRs — their absence from the - cross-reference is intentional (see step 5b) +Overwrites `CHANGES-ISSUES.md` with the current state. Every number renders as +a full `[#N](https://github.com/penpot/penpot/issues/N)` or +`[#N](https://github.com/penpot/penpot/pull/N)` link. -## Version section template +**An anomaly is a milestone mismatch or a `:boom:` mislabel** (the changelog +pairing is misleading and a human must judge intent): -```markdown -## +1. Issue in this milestone, referenced PR in another milestone (or none). +2. PR in this milestone, closed issue in another milestone — except an issue + with *no* milestone, which belongs to another (probably private) project + and is neither anomaly nor changelog candidate. +3. `:boom:` entry whose issue lacks the `breaking change` label. These stay + listed in the report **and** in the changelog: either label the issue or + move the entry to its regular section. -### :bug: Bugs fixed +**Highlight gaps are warnings, not anomalies:** missing `:rocket:` on a +released X.Y.0 (patches never carry highlights); `:rocket:` entry without +issue AND PR references. Gaps never count toward the anomaly total. -- [#](https://github.com/penpot/penpot/issues/) (PR: [#](https://github.com/penpot/penpot/pull/)) -- (by @contributor) [#](https://github.com/penpot/penpot/issues/) (PR: [#](https://github.com/penpot/penpot/pull/)) -- (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX) -``` - -Advisory (GHSA) entries have no issue or PR link — just the advisory URL. See -step 5b. - -### 11. Generate anomaly report and save to CHANGES-ISSUES.md - -After all edits and cross-referencing are complete, generate a structured -report and save it to `CHANGES-ISSUES.md` (overwriting if exists). -This provides a persistent record of any discrepancies between the milestone -and the changelog. - -**Every issue and PR number in the report must be rendered as a full GitHub -Markdown link** using the same URL format as `CHANGES.md`: -- Issue N → `[#N](https://github.com/penpot/penpot/issues/N)` -- PR N → `[#N](https://github.com/penpot/penpot/pull/N)` - -The titles and notes should also link to the corresponding issue/PR page -where applicable, so the report is self-contained and clickable from any -Markdown viewer. - -## What is an anomaly - -**An anomaly is a milestone-mismatch between an issue and its referenced -PR.** There are two anomaly types, plus two highlight gaps (warnings that -do not count toward the anomaly total): - -1. **Issue is in the milestone, but its referenced PR is in a different - milestone (or has no milestone).** The changelog claims a fix in this - release, but the PR is being released elsewhere — the fix may not - actually ship here. -2. **PR is in the milestone, but the issue it closes is in a different - milestone.** The PR is being released here, but the issue it fixes is - being released in a different version — the changelog pairing is - misleading. - - **Exception — issue with no milestone is NOT an anomaly.** Milestones - are only required for issues tracked in the "Main" project. A milestone - PR that closes an issue with no milestone references an issue from - another (probably private) project; that is expected and the issue is - not part of this changelog. Do not report it. -3. **missing-highlights (gap):** A released X.Y.0 version section has no - `### :rocket: Epics and highlights` subsection. Patches (X.Y.Z) never - carry highlights, so only minors/majors are checked. -4. **missing-highlight-reference (gap):** A `:rocket:` entry lacks the - required issue AND PR references. Every highlight entry must follow the - standard changelog format with `[#ISSUE]` and `(PR: [#PR])` links - (multi-PR `(PR: [#A](...), [#B](...))` accepted). - -**Anything else is not an anomaly.** Other discrepancies (exclusion -labels on in-changelog issues, missing valid issues, unmerged PR -references, duplicates across versions, stale milestone assignments) -are **rule violations** that the LLM must fix directly in `CHANGES.md` -during step 6a (pre-flight checks). They should not appear in this -report — if they do, the LLM has skipped the pre-flight step and -needs to re-run the workflow. - -The changelog's primary unit is the **issue**, not the PR, so a missing or -mismatched PR only matters when its issue is part of this milestone. - -Run this self-contained script: - -```bash -python3 << 'PYEOF' -import json, re, subprocess, sys -from datetime import datetime, timezone - -MILESTONE = "" -CHANGES_MD = "CHANGES.md" -OUTPUT = "CHANGES-ISSUES.md" -REPO = "penpot/penpot" - -# --- URL helpers (match CHANGES.md format exactly) --- -def issue_url(n): return f"https://github.com/{REPO}/issues/{n}" -def pr_url(n): return f"https://github.com/{REPO}/pull/{n}" -def issue_link(n): return f"[#{n}]({issue_url(n)})" -def pr_link(n): return f"[#{n}]({pr_url(n)})" -def issue_link_title(n, title): - url = issue_url(n) - if title: - return f"[#{n}]({url}) — [{title}]({url})" - return f"[#{n}]({url})" -def pr_link_title(n, title): - url = pr_url(n) - if title: - return f"[#{n}]({url}) — [{title}]({url})" - return f"[#{n}]({url})" -def fmt_pr_list(nums): - return ", ".join(pr_link(n) for n in nums) -def fmt_issue_list(nums): - return ", ".join(issue_link(n) for n in nums) - -# --- Fetch milestone data --- -result = subprocess.run( - ["python3", "scripts/gh.py", "issues", MILESTONE, "--state", "all"], - capture_output=True, text=True) -all_issues = json.loads(result.stdout) -issue_by_num = {i['number']: i for i in all_issues} - -result = subprocess.run( - ["python3", "scripts/gh.py", "prs", "--milestone", MILESTONE, "--state", "all"], - capture_output=True, text=True) -all_prs = json.loads(result.stdout) -pr_by_num = {p['number']: p for p in all_prs} - -# --- Read changelog section --- -with open(CHANGES_MD) as f: - content = f.read() - -m = re.search(rf'## {re.escape(MILESTONE)}(?:\s*\([^)]*\))?\n(.*?)(?:\n## |\Z)', content, re.DOTALL) -section = m.group(1) if m else "" - -changelog_issues = set() -for num in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/issues/\d+\)', section): - changelog_issues.add(int(num)) -for num in re.findall(r'\[Github #(\d+)\]', section): - changelog_issues.add(int(num)) - -changelog_prs = set() -for num in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)', section): - changelog_prs.add(int(num)) -for num in re.findall(r'PR:\[(\d+)\]', section): - changelog_prs.add(int(num)) - -# --- Milestone lookup caches --- -# PRs and issues returned by milestone queries are KNOWN to be in MILESTONE. -# For everything else, fall back to `gh` per-item lookups. -pr_milestone_cache = {p['number']: MILESTONE for p in all_prs} -issue_milestone_cache = {i['number']: MILESTONE for i in all_issues} - -def get_pr_milestone(pr_num): - """Return the milestone title for a PR, or None if unassigned / unknown.""" - if pr_num in pr_milestone_cache: - return pr_milestone_cache[pr_num] - try: - r = subprocess.run( - ["gh", "pr", "view", str(pr_num), "--json", "milestone"], - capture_output=True, text=True, check=True) - data = json.loads(r.stdout) - ms = data.get('milestone') - pr_milestone_cache[pr_num] = (ms or {}).get('title') - except (subprocess.CalledProcessError, json.JSONDecodeError): - pr_milestone_cache[pr_num] = None - return pr_milestone_cache[pr_num] - -def get_issue_milestone(issue_num): - """Return the milestone title for an issue, or None if unassigned / unknown.""" - if issue_num in issue_milestone_cache: - return issue_milestone_cache[issue_num] - try: - r = subprocess.run( - ["gh", "issue", "view", str(issue_num), "--json", "milestone"], - capture_output=True, text=True, check=True) - data = json.loads(r.stdout) - ms = data.get('milestone') - issue_milestone_cache[issue_num] = (ms or {}).get('title') - except (subprocess.CalledProcessError, json.JSONDecodeError): - issue_milestone_cache[issue_num] = None - return issue_milestone_cache[issue_num] - -# --- Exclusion rules (shared) --- -EXCLUDED_LABELS = {'release blocker', 'no changelog'} -EXCLUDED_ISSUE_TYPES = {'Task'} -EXCLUDED_PROJECT_STATUS = {'Rejected'} - -def issue_excluded(issue): - if not issue: return True - if issue.get('state') != 'CLOSED': return True - if issue.get('issue_type') in EXCLUDED_ISSUE_TYPES: return True - if issue.get('project_status') in EXCLUDED_PROJECT_STATUS: return True - if EXCLUDED_LABELS & set(issue.get('labels', [])): return True - return False - -# --- ANOMALIES: milestone mismatches between issues and their referenced PRs --- -# These are the ONLY items that should appear in the report. All other -# discrepancies (exclusion labels, missing valid issues, unmerged PRs, -# duplicates, stale milestone assignments) are workflow errors that the -# LLM must fix in step 6a (pre-flight checks) — they are not anomalies. - -# Type A: issue in MILESTONE, referenced PR in different milestone or no milestone -anomalies_a = [] # list of dicts: {issue, issue_title, pr, pr_milestone} -for issue_num in sorted(changelog_issues): - issue = issue_by_num.get(issue_num) - if not issue: continue - if get_issue_milestone(issue_num) != MILESTONE: continue - for pr_num in issue.get('closing_prs', []): - pr_ms = get_pr_milestone(pr_num) - if pr_ms != MILESTONE: - anomalies_a.append({ - 'issue': issue_num, - 'issue_title': issue.get('title', ''), - 'pr': pr_num, - 'pr_milestone': pr_ms, # may be None - }) - -# Type B: PR in MILESTONE, the issue it closes is in different milestone or no milestone -anomalies_b = [] # list of dicts: {pr, pr_title, issue, issue_milestone} -for pr_num in sorted(changelog_prs): - pr = pr_by_num.get(pr_num) - if not pr: continue - if get_pr_milestone(pr_num) != MILESTONE: continue - for issue_num in pr.get('closing_issues', []): - issue_ms = get_issue_milestone(issue_num) - # No milestone = issue from another (probably private) project — - # milestones are only required for the "Main" project. Not an - # anomaly, and the issue never belongs in this changelog. - if issue_ms is None: continue - if issue_ms != MILESTONE: - anomalies_b.append({ - 'pr': pr_num, - 'pr_title': pr.get('title', ''), - 'issue': issue_num, - 'issue_milestone': issue_ms, # may be None - }) - -# --- Type C: released X.Y.0 version sections without :rocket: subsection --- -# Patches (X.Y.Z with Z != 0) never carry :rocket: by design — only minors/majors (X.Y.0). -anomalies_c = [] # list of version strings -rocket_heading_re = re.compile(r'^### :rocket:', re.MULTILINE) -version_sections = re.split(r'(?=^## \d+\.\d+\.\d+)', content, flags=re.MULTILINE) -for vs in version_sections: - m = re.match(r'^## (\d+\.\d+\.\d+)(.*)', vs) - if not m: continue - ver, suffix = m.group(1), m.group(2) - if 'unreleased' in suffix.lower(): continue - if ver.split('.')[2] != '0': continue - if not rocket_heading_re.search(vs): - anomalies_c.append(ver) - -# --- Type D: :rocket: entries without issue AND PR references --- -# Both are required: `[#ISSUE](.../issues/N)` and `(PR: [#PR](.../pull/M))`. -# Multi-PR entries `(PR: [#A](...), [#B](...))` are accepted. -anomalies_d = [] # list of dicts: {version, line} -issue_ref_re = re.compile(r'\[#\d+\]\(https://github\.com/penpot/penpot/issues/\d+\)') -pr_ref_re = re.compile(r'\(PR:\s*\[#\d+\]\(https://github\.com/penpot/penpot/pull/\d+\)(\s*,\s*\[#\d+\]\(https://github\.com/penpot/penpot/pull/\d+\))*\)') -for vs in version_sections: - m = re.match(r'^## (\d+\.\d+\.\d+)(.*)', vs) - if not m: continue - ver = m.group(1) - rocket_match = rocket_heading_re.search(vs) - if not rocket_match: continue - # Extract the :rocket: subsection body (up to next ### or ##) - rocket_body = vs[rocket_match.end():] - rocket_body = re.split(r'(?m)^#{2,3}\s', rocket_body)[0] - for line in rocket_body.splitlines(): - line = line.strip() - if line.startswith('- ') and not (issue_ref_re.search(line) and pr_ref_re.search(line)): - anomalies_d.append({'version': ver, 'line': line[:100]}) - -# --- Write report --- -def fmt_ms(ms): - return ms if ms else "_none_" - -with open(OUTPUT, 'w') as f: - f.write(f'# Changelog Anomaly Report — {MILESTONE}\n\n') - f.write(f'Generated: {datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")}\n\n') - f.write('---\n\n') - - n_a = len(anomalies_a) - n_b = len(anomalies_b) - n_c = len(anomalies_c) - n_d = len(anomalies_d) - - f.write('## Summary\n\n') - f.write(f'- **Issue in {MILESTONE}, referenced PR in different milestone or no milestone:** {n_a}\n') - f.write(f'- **PR in {MILESTONE}, closing issue in a different milestone:** {n_b}\n') - f.write(f'- **Total anomalies:** {n_a + n_b}\n') - f.write(f'- **Released X.Y.0 version missing :rocket: section (gap):** {n_c}\n') - f.write(f'- **:rocket: entry without issue AND PR references (gap):** {n_d}\n\n') - - # --- Anomalies section (milestone mismatches only) --- - if n_a or n_b: - f.write('## Anomalies\n\n') - f.write('These are milestone mismatches between an issue in the changelog ' - 'and its referenced PR (or vice-versa). The changelog claim ' - '"this issue is fixed by this PR, all in this milestone" is ' - 'inconsistent with the actual milestone assignments. ' - 'Resolve by either updating the milestone on the issue/PR or ' - 'removing the misleading entry from the changelog.\n\n') - - if n_a: - f.write(f'### Issue in {MILESTONE}, PR in different milestone or no milestone\n\n') - by_issue = {} - for a in anomalies_a: - by_issue.setdefault(a['issue'], []).append(a) - for issue_num in sorted(by_issue): - entries = by_issue[issue_num] - title = entries[0]['issue_title'] - f.write(f'- {issue_link_title(issue_num, title[:80])}\n') - for e in entries: - ms_label = fmt_ms(e['pr_milestone']) - badge = '🔴' if e['pr_milestone'] is None else '⚠️' - f.write(f' - {badge} Referenced {pr_link(e["pr"])} is in milestone **{ms_label}** (expected: {MILESTONE})\n') - f.write('\n') - - if n_b: - f.write(f'\n### PR in {MILESTONE}, closing issue in a different milestone\n\n') - by_pr = {} - for b in anomalies_b: - by_pr.setdefault(b['pr'], []).append(b) - for pr_num in sorted(by_pr): - entries = by_pr[pr_num] - title = entries[0]['pr_title'] - f.write(f'- {pr_link_title(pr_num, title[:80])}\n') - for e in entries: - ms_label = fmt_ms(e['issue_milestone']) - badge = '🔴' if e['issue_milestone'] is None else '⚠️' - f.write(f' - {badge} Closing {issue_link(e["issue"])} is in milestone **{ms_label}** (expected: {MILESTONE})\n') - f.write('\n') - - else: - f.write('✅ No anomalies found. All (issue, PR) pairs in the changelog have aligned milestone assignments.\n\n') - - # --- Highlight gaps (warnings, not anomalies) --- - if n_c or n_d: - f.write('## Highlight gaps\n\n') - f.write('These are warnings, not anomalies: they do not affect the ' - 'milestone-mismatch total above. They track `:rocket:` coverage ' - 'across all released X.Y.0 versions. Historical entries (e.g. ' - 'Taiga links) predate the current reference convention and are ' - 'expected to appear here.\n\n') - - if n_c: - f.write(f'### Released X.Y.0 version missing :rocket: section\n\n') - f.write('These released minors/majors have no `### :rocket: Epics and highlights` subsection. ' - 'Add highlights to help self-hosted users understand what they are missing.\n\n') - for ver in anomalies_c: - f.write(f'- Version **{ver}**\n') - f.write('\n') - - if n_d: - f.write(f'### :rocket: entry without issue AND PR references\n\n') - f.write('These highlight entries lack the required issue AND PR references. ' - 'Add `[#ISSUE](...)` and `(PR: [#PR](...))` links.\n\n') - for d in anomalies_d: - f.write(f'- **{d["version"]}**: `{d["line"]}`\n') - f.write('\n') - elif not (n_a or n_b): - f.write('✅ No highlight gaps found. All released X.Y.0 versions have properly referenced :rocket: entries.\n\n') - - # --- Context --- - f.write('---\n\n') - f.write('## Context\n\n') - f.write(f'- Milestone: **{MILESTONE}**\n') - f.write(f'- Milestone total issues (all states): {len(all_issues)}\n') - f.write(f'- Closed issues in milestone: {sum(1 for i in all_issues if i.get("state") == "CLOSED")}\n') - f.write(f'- Valid issues after exclusions (after step 5/6a): {len([i for i in all_issues if not issue_excluded(i)])}\n') - f.write(f'- Issues referenced in changelog: {len(changelog_issues)}\n') - f.write(f'- PRs referenced in changelog: {len(changelog_prs)}\n') - -print(f"Anomaly report written to {OUTPUT}") -PYEOF -``` - -This generates `CHANGES-ISSUES.md` containing anomalies and highlight gaps: - -1. **Issue in milestone, referenced PR in different milestone or no milestone** — - the changelog claims a fix here, but the PR is released elsewhere. -2. **PR in milestone, closing issue in a different milestone** — - the PR is released here, but the issue it fixes belongs to another version. - (An issue with *no* milestone belongs to another, probably private, - project — milestones are only required on the "Main" project — so it is - neither an anomaly nor a changelog candidate.) -3. **missing-highlights (gap, warning)** — a released X.Y.0 version section - has no `### :rocket: Epics and highlights` subsection. Patches (X.Y.Z) - never carry highlights. -4. **missing-highlight-reference (gap, warning)** — a `:rocket:` entry lacks - the required issue AND PR references. - -Gaps do not count toward the anomaly total. - -**Rule violations are not in the report** — they are workflow errors the -LLM must fix directly in `CHANGES.md` during step 6a (pre-flight checks). -If the report contains a rule violation, the LLM has skipped the pre-flight -step and needs to re-run the workflow before re-generating the report. - -The report is overwritten each time it's generated, reflecting the current -state of the milestone and changelog. Every number is rendered as a full -`[#N](https://github.com/penpot/penpot/issues/N)` or -`[#N](https://github.com/penpot/penpot/pull/N)` link so the report is -self-contained and clickable in any Markdown viewer. +**Anything else is a rule violation**, not a report item: fix it in step 6 +pre-flight. If one shows up in the report, re-run the workflow. ## Key Principles -- **Issue = changelog unit.** The primary link always points to the - user-facing issue, not the implementation PR. -- **PR = implementation detail.** Reference the PR inline so readers - can find the code changes. -- **Latest version first.** New sections are inserted at the top of the - changelog, below the `# CHANGELOG` header. -- **Issue Type determines section — exclusively.** Use the `issue_type` field from `gh.py` output (Bug → `:bug:`, Feature/Enhancement → `:sparkles:`). **Do not** use labels (`bug`, `enhancement`) or title emoji prefixes (`:bug:`, `:sparkles:`) — they are frequently wrong or contradictory. The `issue_type` is the single source of truth. -- **User-facing descriptions.** Write from the user's perspective — describe - what broke and what was fixed, not internal implementation details. -- **Community attribution.** When the issue or fix PR has the - `community contribution` label, add `(by @)` on the entry line - between the description and the issue link. Use the **PR author** (not the - issue author) for the attribution. -- **Only closed issues.** An issue must have `state: "closed"` to appear in - the changelog. Open/unresolved issues are omitted. -- **Rejected project status.** Issues marked as "Rejected" in the "Main" - project board are automatically excluded by `gh.py`, even if they are - closed. The project status is distinct from the GitHub issue state. - Use `--include-rejected` to override this behavior. -- **Excluded issues.** Issues with `no changelog` label must be excluded. - Issues with `issue_type: "Task"` must also be excluded — they are internal - chores, not user-facing changes. -- **Multiple PRs per issue.** If multiple PRs fix the same issue, list them - comma-separated inline: `(PR: [#A](url), [#B](url))`. -- **Duplicate removal.** If an entry already exists in a prior version section, - remove it from the current version. Check for text-level duplicates (after - stripping links and attributions) across version sections. -- **Taiga references.** If a changelog entry references a Taiga URL - (`tree.taiga.io`), attempt to find a corresponding GitHub issue via the - Taiga description text or by searching GitHub PRs that reference the Taiga - URL. Replace the Taiga reference with the GitHub issue link and add the PR - reference if applicable. -- **Security advisory (GHSA) entries.** Advisories fixed in the release are - listed under `### :bug: Bugs fixed` with the advisory URL and **no issue or - PR link**, even though they are not in the milestone. The GHSA ID and - description come from the user — do **not** fetch or verify the URL, and do - not drop a draft (unpublished) advisory. Precedent: - `- Fix arbitrary file read security issue on create-font-variant rpc method - (https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)`. - See step 5b. -- **Re-fetch before editing.** Milestones can change — always re-fetch issues - before making edits, don't rely on cached data. -- **Use `scripts/gh.py`.** Prefer the helper script over raw `gh api` calls for - milestone issue listing and PR detail fetching. It handles GraphQL - pagination, batching, and label filtering automatically. -- **Verify PR merge status.** Not all closing PRs are merged — community PRs - can be superseded and closed without merging. Always check that every PR - referenced in the changelog has `state: MERGED`. -- **PR-level exclusions apply.** A PR can carry its own exclusion labels - (`release blocker`, `no issue required`) independent of its linked issue's - labels. Check both. -- **Cross-reference milestone PRs, not just issues.** The `--compare` flag on - the `issues` command only compares issue numbers. Merged PRs not linked to - any milestone issue can be missed. Use `python3 scripts/gh.py prs --milestone` - for a full PR cross-reference. -- **False-positive PR-to-issue associations.** A PR may claim to close an - issue from a different project or context. If the PR title and issue title - are clearly unrelated, or the PR predates the issue by years, treat it as a - data glitch and skip it. -- **Anomaly = milestone mismatch only; gaps are warnings.** The report's - anomaly total counts only milestone mismatches: (1) the issue is in this - milestone but the referenced PR is in a different milestone (or unassigned), - and (2) the PR is in this milestone but the issue it closes is in a - different milestone. `:rocket:` highlight gaps (missing section on a - released X.Y.0, entry without issue AND PR references) are reported in a - separate `Highlight gaps` section and never count toward the anomaly total. An - *unassigned* (milestone-less) issue closed by a milestone PR is **not** - an anomaly: milestones are required only for the "Main" project, so such - issues come from another (probably private) project and are not changelog - candidates. These anomalies are reported because the changelog pairing is - *misleading* — the human needs to decide whether the milestone or the - changelog is wrong. All other discrepancies (exclusion labels, missing - valid issues, unmerged PR references, duplicates, stale milestone - assignments) are **rule violations** that the LLM must fix directly in - `CHANGES.md` during step 6a (pre-flight checks). They never appear in - the report — if they do, the pre-flight step was skipped. +- **Issue = changelog unit**, PR = implementation detail inline. +- **Latest version first**, below the `# CHANGELOG` header. +- **Issue Type decides the section — exclusively**, except `breaking change` + label → `:boom:` first. +- **User-facing descriptions** in imperative mood. +- **Community attribution** uses the PR author, placed before the issue link. +- **Only closed issues**; **Rejected** project status excludes. +- **`no changelog` and `Task`** stay out. +- **Multiple fix PRs** go comma-separated inline. +- **Duplicates**: the earlier version section wins. +- **Taiga references**: resolve to the GitHub issue via description text or + PRs mentioning the Taiga URL, then link issue + PR. +- **GHSA entries** live under `:bug:` with only the advisory URL (see 5b). +- **Re-fetch before editing**; prefer `scripts/gh.py` over raw `gh api`. +- **Verify PR merge status** (`check-merged`); PR-level exclusions apply. +- **Cross-reference PRs, not just issues** (`cross-ref`); watch for + false-positive PR-to-issue links. diff --git a/.serena/memories/critical-info.md b/.serena/memories/critical-info.md index f0cec128dc..5f1b52c81b 100644 --- a/.serena/memories/critical-info.md +++ b/.serena/memories/critical-info.md @@ -55,6 +55,7 @@ The memory is structured in a way that you can get the critical information abou - `scripts/psql` — PostgreSQL client wrapper with devenv defaults. Companion: `scripts/db-schema` for DDL dumps. See `mem:scripts/psql`. - `scripts/taiga.py` — Fetch public issues, user stories, and tasks from the Penpot Taiga project without authentication. See `mem:scripts/taiga`. - `scripts/gh.py` — GitHub operations helper: list milestone issues, fetch PR details, compare against CHANGES.md. Requires `gh` CLI. See `mem:scripts/gh`. +- `scripts/changelog.py` — Changelog checks for the `update-changelog` skill: `check-merged` (PR merge status), `cross-ref` (milestone PRs vs changelog section), `report` (anomaly report to CHANGES-ISSUES.md). Calls `scripts/gh.py` via subprocess. See `mem:scripts/changelog`. - `scripts/error-reports.mjs` — Query error reports via RPC API with token authentication. Supports list/get operations with filtering and pagination. See `mem:scripts/error-reports`. - `scripts/clean-node-modules` — Remove stale `node_modules` from all pnpm workspaces (root, modules, member packages). Keeps the shared pnpm store at `/.pnpm-store` unless `--store`; ignores `external/` and `.opencode/`. Usage and reinstall steps: `mem:workflow/updating-pnpm`. - `scripts/ci` — CI orchestration script: runs lint, tests, and format checks per module (`frontend backend common render-wasm exporter mcp plugins library`). Logs go to `.ci-logs/`; read the log file on failure. See `mem:scripts/ci`. diff --git a/.serena/memories/scripts/changelog.md b/.serena/memories/scripts/changelog.md new file mode 100644 index 0000000000..6dc0dd5c94 --- /dev/null +++ b/.serena/memories/scripts/changelog.md @@ -0,0 +1,18 @@ +# Changelog helper + +`scripts/changelog.py` holds the checks used by the `update-changelog` skill (extracted from the skill file so the skill stays workflow-only). Calls `scripts/gh.py` via subprocess; never call the GitHub API directly from this script. + +## Subcommands + +- `check-merged ` (`--file`, `--stdin` accepted) — warn on any non-merged PR, exit 1 when found; run before writing changelog entries. +- `cross-ref [--changes CHANGES.md]` — fetch milestone PRs with `--state all` once, report merged PRs missing from the version section plus CLOSED (unmerged) warnings; GHSA entries never appear here by design. +- `report [--changes CHANGES.md --output CHANGES-ISSUES.md]` — overwrite the anomaly report (milestone mismatches type A/B plus `:rocket:` gaps C/D); rule violations never belong in the report, they are fixed in CHANGES.md first. + +## Conventions + +- Milestone resolution is batched, never per-item: unknown PRs go through `gh.py prs ` and unknown issues through `gh.py issue ` (one GraphQL call per 50 items); a lookup miss resolves to null, never to an extra call. + +- Version-section lookup accepts an optional `(suffix)` after the version header (e.g. `(Unreleased)`); a missing section is an error for `cross-ref` and an empty section for `report`. +- PR/issue reference parsing covers standard `[#N](.../pull/N)` / `[#N](.../issues/N)` links plus legacy `PR:[N]` / `[Github #N]` forms. +- Shared parsing helpers (`extract_version_section`, `extract_subsection`, `collect_changelog_prs/issues`) live at module top so future subcommands reuse them instead of duplicating regexes. +- Report anomaly types: A/B milestone mismatches, E `:boom:` entry whose issue lacks the `breaking change` label (counted, entries preserved — human labels the issue or moves the entry); C/D are `:rocket:` gaps (warnings, never counted). diff --git a/.serena/memories/scripts/gh.md b/.serena/memories/scripts/gh.md index ef061edd60..d0546e5f13 100644 --- a/.serena/memories/scripts/gh.md +++ b/.serena/memories/scripts/gh.md @@ -53,7 +53,7 @@ python3 scripts/gh.py issues "2.16.0" --compare CHANGES.md ### `prs` -Fetch PR details by number or by milestone. +Fetch PR details by number or by milestone. Every entry includes a `milestone` field (title or null). ```bash # Fetch specific PRs @@ -74,6 +74,12 @@ python3 scripts/gh.py prs --milestone "2.16.0" --state all **Output**: JSON array to stdout; progress to stderr. +### `issue` + +Fetch issue details by number in batches of 50 (same input styles as `prs`: numbers, `--file`, `--stdin`). No filters. A dead number fails its whole batch (unlike PRs, issues error instead of resolving to null), so the batch falls back to one-by-one lookups and reports misses as `{"number", "error": "not_found"}`. Any other failure aborts. + +**Output**: JSON array to stdout; progress to stderr. + ### `link-issue` Explicitly assign a GitHub issue to a pull request and verify the relationship from both sides: @@ -111,7 +117,10 @@ python3 scripts/gh.py advisories GHSA-xvj6-fh9w-gjw7 ## Key principles +- Changelog-specific checks (merge-status verification, milestone-vs-changelog cross-reference, anomaly report) live in `scripts/changelog.py`, which calls this script via subprocess — see `mem:scripts/changelog`. + - All output is JSON — pipe into `jq` or other tools for further processing. +- Transient HTTP 504 responses from `gh` are retried automatically (3 attempts, 5s/15s backoff); any other error fails fast with no retry. - Milestone lookup is by exact title match. - `issues` subcommand auto-paginates (100 items per page). - `prs` subcommand batches PR number lookups (50 per GraphQL query). diff --git a/CHANGES.md b/CHANGES.md index c48637725a..6ded71352f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -257,6 +257,7 @@ - Use hard reload for render engine switching in the workspace menu [#10441](https://github.com/penpot/penpot/issues/10441) (PR: [#10444](https://github.com/penpot/penpot/pull/10444)) - Rotate size badge when shape is rotated [#10386](https://github.com/penpot/penpot/issues/10386) (PR: [#10393](https://github.com/penpot/penpot/pull/10393)) - Add separate internal URI for exporter to handle Docker deployments where internal and public URIs differ [#10627](https://github.com/penpot/penpot/issues/10627) (PR: [#10630](https://github.com/penpot/penpot/pull/10630)) +- Make throwValidationErrors default to true for v2 manifest plugins [#10401](https://github.com/penpot/penpot/issues/10401) (PR: [#10433](https://github.com/penpot/penpot/pull/10433)) ### :bug: Bugs fixed @@ -364,6 +365,14 @@ - Fix text shape position-data to include required fills in WASM and DOM calculation paths [#10646](https://github.com/penpot/penpot/issues/10646) (PR: [#10650](https://github.com/penpot/penpot/pull/10650)) - Log expired OIDC tokens as auth failures instead of server errors [#10635](https://github.com/penpot/penpot/issues/10635) (PR: [#10636](https://github.com/penpot/penpot/pull/10636)) - Return 400 instead of 500 when ImageMagick rejects invalid uploaded images [#10642](https://github.com/penpot/penpot/issues/10642) (PR: [#10643](https://github.com/penpot/penpot/pull/10643)) +- Fix Plugin API board.guides setting and clearing throwing malli invalid-schema error (by @filipsajdak) [#9773](https://github.com/penpot/penpot/issues/9773) (PR: [#10503](https://github.com/penpot/penpot/pull/10503)) +- Fix Plugin API rejecting open-overlay and toggle-overlay interactions when position is not set (by @mvanhorn) [#10201](https://github.com/penpot/penpot/issues/10201) (PR: [#10503](https://github.com/penpot/penpot/pull/10503)) +- Fix Plugin API close-overlay interaction requiring animation field (by @mvanhorn) [#10202](https://github.com/penpot/penpot/issues/10202) (PR: [#10503](https://github.com/penpot/penpot/pull/10503)) +- Fix boards hidden from View Mode reappearing when adding prototype interactions (by @jeffrey701) [#10289](https://github.com/penpot/penpot/issues/10289) (PR: [#9695](https://github.com/penpot/penpot/pull/9695)) +- Fix Plugin API PenpotUtils.createVariantContainer assigning variant properties to wrong components [#10506](https://github.com/penpot/penpot/issues/10506) (PR: [#10562](https://github.com/penpot/penpot/pull/10562)) +- Fix mask inside flex layout shifting position or size when child image visibility changes [#10537](https://github.com/penpot/penpot/issues/10537) (PR: [#10697](https://github.com/penpot/penpot/pull/10697)) +- Fix path ends selector missing arrow icons and incorrect divider colors [#10593](https://github.com/penpot/penpot/issues/10593) (PR: [#10631](https://github.com/penpot/penpot/pull/10631)) +- Fix webhook form prefilling hardcoded metadata URI default [#10722](https://github.com/penpot/penpot/issues/10722) (PR: [#10723](https://github.com/penpot/penpot/pull/10723)) ## 2.16.2 diff --git a/scripts/changelog.py b/scripts/changelog.py new file mode 100644 index 0000000000..b9106f201f --- /dev/null +++ b/scripts/changelog.py @@ -0,0 +1,616 @@ +#!/usr/bin/env python3 +""" +changelog.py — Helper commands for the ``update-changelog`` skill. + +Uses ``scripts/gh.py`` (via subprocess) and the local ``CHANGES.md`` so the +skill file itself stays free of inline programs. + +Subcommands: + check-merged Verify that every given PR is merged (warns otherwise) + cross-ref Compare merged milestone PRs against the changelog section + report Generate the anomaly report (CHANGES-ISSUES.md) + +Usage: + python3 scripts/changelog.py check-merged 9179 9204 9311 + cat prs.txt | python3 scripts/changelog.py check-merged --stdin + python3 scripts/changelog.py cross-ref "2.16.0" + python3 scripts/changelog.py report "2.16.0" + python3 scripts/changelog.py report "2.16.0" --changes CHANGES.md --output CHANGES-ISSUES.md + +Prerequisites: + - gh CLI authenticated (gh auth status) + - Python 3.8+ +""" + +import argparse +import json +import re +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + + +REPO = "penpot/penpot" +GH_PY = Path(__file__).resolve().parent / "gh.py" + + +# ───────────────────────────────────────────── +# Shared helpers +# ───────────────────────────────────────────── + + +def run_gh_py(*args: str, input_text: str | None = None) -> str: + """Run ``scripts/gh.py`` with the given arguments, return stdout.""" + cmd = [sys.executable, str(GH_PY), *args] + result = subprocess.run(cmd, input=input_text, capture_output=True, text=True) + if result.returncode != 0: + print(f"gh.py error: {result.stderr}", file=sys.stderr) + sys.exit(1) + return result.stdout + + +def read_pr_numbers(args: argparse.Namespace) -> list[int]: + """Collect PR numbers from positional args, --file, and/or --stdin.""" + numbers: list[int] = [] + if args.numbers: + numbers.extend(args.numbers) + if args.file: + with open(args.file) as f: + for line in f: + line = line.strip() + if line: + numbers.append(int(line)) + if args.stdin: + for line in sys.stdin: + line = line.strip() + if line: + numbers.append(int(line)) + seen: set[int] = set() + return [n for n in numbers if not (n in seen or seen.add(n))] + + +def fetch_prs_by_numbers(pr_numbers: list[int]) -> list[dict]: + """Fetch PR details via ``gh.py prs`` for explicit PR numbers.""" + stdout = run_gh_py("prs", *[str(n) for n in pr_numbers]) + return json.loads(stdout) + + +def fetch_milestone_prs(milestone: str, state: str) -> list[dict]: + """Fetch all PRs in a milestone via ``gh.py prs --milestone``.""" + stdout = run_gh_py("prs", "--milestone", milestone, "--state", state) + return json.loads(stdout) + + +def extract_version_section(content: str, milestone: str) -> str: + """Return the changelog body of a ``## `` section (or "").""" + match = re.search( + rf"^## {re.escape(milestone)}(?:\s*\([^)]*\))?\n(.*?)(?=^## |\Z)", + content, + re.DOTALL | re.MULTILINE, + ) + return match.group(1) if match else "" + + +def extract_subsection(section: str, heading_re: str) -> str: + """Return the body of a ``### `` subsection (or "").""" + match = re.search(rf"^{heading_re}", section, re.MULTILINE) + if not match: + return "" + body = section[match.end():] + return re.split(r"(?m)^#{2,3}\s", body)[0] + + +def collect_changelog_prs(section: str) -> set[int]: + """Collect all PR numbers referenced in a changelog section.""" + numbers = set() + for num in re.findall( + r"\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)", section + ): + numbers.add(int(num)) + for num in re.findall(r"PR:\[(\d+)\]", section): + numbers.add(int(num)) + return numbers + + +def collect_changelog_issues(section: str) -> set[int]: + """Collect all issue numbers referenced in a changelog section.""" + numbers = set() + for num in re.findall( + r"\[#(\d+)\]\(https://github\.com/penpot/penpot/issues/\d+\)", section + ): + numbers.add(int(num)) + for num in re.findall(r"\[Github #(\d+)\]", section): + numbers.add(int(num)) + return numbers + + +# ───────────────────────────────────────────── +# Subcommand: check-merged +# ───────────────────────────────────────────── + + +def cmd_check_merged(args: argparse.Namespace) -> None: + """Warn about any given PR that is not merged. Exits 1 when found.""" + pr_numbers = read_pr_numbers(args) + if not pr_numbers: + print( + "ERROR: no PR numbers provided (pass numbers, --file, or --stdin)", + file=sys.stderr, + ) + sys.exit(1) + + prs = fetch_prs_by_numbers(pr_numbers) + bad = 0 + for pr in prs: + number = pr.get("number") + state = pr.get("state") + if state != "MERGED": + bad += 1 + print(f"WARNING: #{number} is {state} (not merged)") + + if bad: + print(f"{bad} of {len(prs)} PRs are NOT merged", file=sys.stderr) + sys.exit(1) + print(f"OK: all {len(prs)} PRs are merged", file=sys.stderr) + + +# ───────────────────────────────────────────── +# Subcommand: cross-ref +# ───────────────────────────────────────────── + + +def cmd_cross_ref(args: argparse.Namespace) -> None: + """Compare merged milestone PRs against the changelog section.""" + with open(args.changes) as f: + content = f.read() + section = extract_version_section(content, args.milestone) + if not section: + print( + f'ERROR: no "## {args.milestone}" section found in {args.changes}', + file=sys.stderr, + ) + sys.exit(1) + + changelog_refs = collect_changelog_prs(section) + milestone_prs = fetch_milestone_prs(args.milestone, "all") + + merged = {pr["number"] for pr in milestone_prs if pr.get("state") == "MERGED"} + closed = [pr for pr in milestone_prs if pr.get("state") == "CLOSED"] + + missing = sorted(merged - changelog_refs) + print(f"Milestone merged PRs: {len(merged)}") + print(f"Changelog referenced PRs: {len(changelog_refs)}") + print(f"PRs in milestone but NOT in changelog: {len(missing)}") + for num in missing: + pr = next(p for p in milestone_prs if p["number"] == num) + print(f" #{num} {pr['title'][:80]}") + + if closed: + print("WARNING: CLOSED (unmerged) PRs in milestone:") + for pr in closed: + print(f" #{pr['number']} {pr['title'][:80]}") + + +# ───────────────────────────────────────────── +# Subcommand: report +# ───────────────────────────────────────────── + + +def issue_url(n: int) -> str: + return f"https://github.com/{REPO}/issues/{n}" + + +def pr_url(n: int) -> str: + return f"https://github.com/{REPO}/pull/{n}" + + +def issue_link(n: int) -> str: + return f"[#{n}]({issue_url(n)})" + + +def pr_link(n: int) -> str: + return f"[#{n}]({pr_url(n)})" + + +def issue_link_title(n: int, title: str) -> str: + url = issue_url(n) + if title: + return f"[#{n}]({url}) — [{title}]({url})" + return f"[#{n}]({url})" + + +def pr_link_title(n: int, title: str) -> str: + url = pr_url(n) + if title: + return f"[#{n}]({url}) — [{title}]({url})" + return f"[#{n}]({url})" + + +def cmd_report(args: argparse.Namespace) -> None: + """Generate the changelog anomaly report and write it to a file.""" + milestone = args.milestone + + all_issues = json.loads(run_gh_py("issues", milestone, "--state", "all")) + issue_by_num = {i["number"]: i for i in all_issues} + + all_prs = fetch_milestone_prs(milestone, "all") + pr_by_num = {p["number"]: p for p in all_prs} + + with open(args.changes) as f: + content = f.read() + section = extract_version_section(content, milestone) + changelog_issues = collect_changelog_issues(section) + changelog_prs = collect_changelog_prs(section) + + # PRs and issues returned by milestone queries are KNOWN to be in the + # milestone. Everything else is resolved in bulk via batched gh.py + # lookups (one GraphQL call per 50 items, never one call per item). + pr_milestone_cache = {p["number"]: milestone for p in all_prs} + issue_milestone_cache = {i["number"]: milestone for i in all_issues} + + unknown_prs = set() + for issue in issue_by_num.values(): + unknown_prs.update(issue.get("closing_prs", [])) + unknown_prs -= set(pr_milestone_cache) + if unknown_prs: + print( + f"Resolving milestones for {len(unknown_prs)} PRs outside {milestone}...", + file=sys.stderr, + ) + for pr in json.loads(run_gh_py("prs", *[str(n) for n in sorted(unknown_prs)])): + pr_milestone_cache[pr["number"]] = pr.get("milestone") + + unknown_issues = set() + for pr in pr_by_num.values(): + unknown_issues.update(pr.get("closing_issues", [])) + unknown_issues |= collect_changelog_issues(extract_subsection(section, r"### :boom:")) + unknown_issues -= set(issue_milestone_cache) + external_issues: dict[int, dict] = {} + if unknown_issues: + print( + f"Resolving milestones for {len(unknown_issues)} issues outside {milestone}...", + file=sys.stderr, + ) + for issue in json.loads( + run_gh_py("issue", *[str(n) for n in sorted(unknown_issues)]) + ): + issue_milestone_cache[issue["number"]] = issue.get("milestone") + if "error" not in issue: + external_issues[issue["number"]] = issue + + def get_issue_labels(issue_num: int) -> list[str] | None: + """Return the labels of an issue, or None when they cannot be known.""" + issue = issue_by_num.get(issue_num) + if issue is not None: + return issue.get("labels", []) + issue = external_issues.get(issue_num) + if issue is not None: + return issue.get("labels", []) + return None + + def get_pr_milestone(pr_num: int) -> str | None: + """Return the milestone title for a PR, or None if unassigned.""" + return pr_milestone_cache.get(pr_num) + + def get_issue_milestone(issue_num: int) -> str | None: + """Return the milestone title for an issue, or None if unassigned.""" + return issue_milestone_cache.get(issue_num) + + excluded_labels = {"release blocker", "no changelog"} + + def issue_excluded(issue: dict | None) -> bool: + if not issue: + return True + if issue.get("state") != "CLOSED": + return True + if issue.get("issue_type") in {"Task"}: + return True + if issue.get("project_status") in {"Rejected"}: + return True + if excluded_labels & set(issue.get("labels", [])): + return True + return False + + # Type A: issue in milestone, referenced PR in different milestone or none + anomalies_a = [] + for issue_num in sorted(changelog_issues): + issue = issue_by_num.get(issue_num) + if not issue: + continue + if get_issue_milestone(issue_num) != milestone: + continue + for pr_num in issue.get("closing_prs", []): + pr_ms = get_pr_milestone(pr_num) + if pr_ms != milestone: + anomalies_a.append( + { + "issue": issue_num, + "issue_title": issue.get("title", ""), + "pr": pr_num, + "pr_milestone": pr_ms, + } + ) + + # Type B: PR in milestone, the issue it closes is in a different milestone. + # An issue with NO milestone belongs to another (probably private) project + # and is NOT an anomaly. + anomalies_b = [] + for pr_num in sorted(changelog_prs): + pr = pr_by_num.get(pr_num) + if not pr: + continue + if get_pr_milestone(pr_num) != milestone: + continue + for issue_num in pr.get("closing_issues", []): + issue_ms = get_issue_milestone(issue_num) + if issue_ms is None: + continue + if issue_ms != milestone: + anomalies_b.append( + { + "pr": pr_num, + "pr_title": pr.get("title", ""), + "issue": issue_num, + "issue_milestone": issue_ms, + } + ) + + # Type E: :boom: entry whose issue lacks the `breaking change` label. + # These stay in the changelog (they are preserved, not removed) — the + # human decides whether to label the issue or move the entry elsewhere. + anomalies_e = [] + boom_body = extract_subsection(section, r"### :boom:") + if boom_body: + for issue_num in sorted(collect_changelog_issues(boom_body)): + labels = get_issue_labels(issue_num) + if labels is None: + continue + if "breaking change" not in labels: + issue = issue_by_num.get(issue_num) or external_issues.get(issue_num) or {} + anomalies_e.append( + { + "issue": issue_num, + "issue_title": issue.get("title", ""), + } + ) + + # Type C: released X.Y.0 sections without a :rocket: subsection. + # Patches (X.Y.Z with Z != 0) never carry :rocket: — only minors/majors. + anomalies_c = [] + rocket_heading_re = re.compile(r"^### :rocket:", re.MULTILINE) + version_sections = re.split( + r"(?=^## \d+\.\d+\.\d+)", content, flags=re.MULTILINE + ) + for vs in version_sections: + m = re.match(r"^## (\d+\.\d+\.\d+)(.*)", vs) + if not m: + continue + ver, suffix = m.group(1), m.group(2) + if "unreleased" in suffix.lower(): + continue + if ver.split(".")[2] != "0": + continue + if not rocket_heading_re.search(vs): + anomalies_c.append(ver) + + # Type D: :rocket: entries without issue AND PR references. + anomalies_d = [] + issue_ref_re = re.compile( + r"\[#\d+\]\(https://github\.com/penpot/penpot/issues/\d+\)" + ) + pr_ref_re = re.compile( + r"\(PR:\s*\[#\d+\]\(https://github\.com/penpot/penpot/pull/\d+\)" + r"(\s*,\s*\[#\d+\]\(https://github\.com/penpot/penpot/pull/\d+\))*\)" + ) + for vs in version_sections: + m = re.match(r"^## (\d+\.\d+\.\d+)(.*)", vs) + if not m: + continue + ver = m.group(1) + rocket_body = extract_subsection(vs, r"### :rocket:") + if not rocket_body: + continue + for line in rocket_body.splitlines(): + line = line.strip() + if line.startswith("- ") and not ( + issue_ref_re.search(line) and pr_ref_re.search(line) + ): + anomalies_d.append({"version": ver, "line": line[:100]}) + + def fmt_ms(ms: str | None) -> str: + return ms if ms else "_none_" + + with open(args.output, "w") as f: + f.write(f"# Changelog Anomaly Report — {milestone}\n\n") + f.write( + f"Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}\n\n" + ) + f.write("---\n\n") + + n_a, n_b, n_c, n_d, n_e = ( + len(anomalies_a), + len(anomalies_b), + len(anomalies_c), + len(anomalies_d), + len(anomalies_e), + ) + + f.write("## Summary\n\n") + f.write( + f"- **Issue in {milestone}, referenced PR in different milestone or no milestone:** {n_a}\n" + ) + f.write( + f"- **PR in {milestone}, closing issue in a different milestone:** {n_b}\n" + ) + f.write(f"- **:boom: entry without breaking change label:** {n_e}\n") + f.write(f"- **Total anomalies:** {n_a + n_b + n_e}\n") + f.write( + f"- **Released X.Y.0 version missing :rocket: section (gap):** {n_c}\n" + ) + f.write(f"- **:rocket: entry without issue AND PR references (gap):** {n_d}\n\n") + + if n_a or n_b or n_e: + f.write("## Anomalies\n\n") + f.write( + "Types A and B are milestone mismatches between an issue in the changelog " + "and its referenced PR (or vice-versa). The changelog claim " + '"this issue is fixed by this PR, all in this milestone" is ' + "inconsistent with the actual milestone assignments. " + "Resolve by either updating the milestone on the issue/PR or " + "removing the misleading entry from the changelog.\n\n" + "Type E entries stay in the changelog: either label the issue " + "as `breaking change` or move the entry out of `:boom:`.\n\n" + ) + + if n_a: + f.write( + f"### Issue in {milestone}, PR in different milestone or no milestone\n\n" + ) + by_issue: dict[int, list[dict]] = {} + for a in anomalies_a: + by_issue.setdefault(a["issue"], []).append(a) + for issue_num in sorted(by_issue): + entries = by_issue[issue_num] + title = entries[0]["issue_title"] + f.write(f"- {issue_link_title(issue_num, title[:80])}\n") + for e in entries: + ms_label = fmt_ms(e["pr_milestone"]) + badge = "🔴" if e["pr_milestone"] is None else "⚠️" + f.write( + f" - {badge} Referenced {pr_link(e['pr'])} is in milestone **{ms_label}** (expected: {milestone})\n" + ) + f.write("\n") + + if n_b: + f.write( + f"\n### PR in {milestone}, closing issue in a different milestone\n\n" + ) + by_pr: dict[int, list[dict]] = {} + for b in anomalies_b: + by_pr.setdefault(b["pr"], []).append(b) + for pr_num in sorted(by_pr): + entries = by_pr[pr_num] + title = entries[0]["pr_title"] + f.write(f"- {pr_link_title(pr_num, title[:80])}\n") + for e in entries: + ms_label = fmt_ms(e["issue_milestone"]) + badge = "🔴" if e["issue_milestone"] is None else "⚠️" + f.write( + f" - {badge} Closing {issue_link(e['issue'])} is in milestone **{ms_label}** (expected: {milestone})\n" + ) + f.write("\n") + + if n_e: + f.write("\n### :boom: entry without breaking change label\n\n") + f.write( + "These entries sit under `### :boom: Breaking changes & Deprecations` " + "but their issue has no `breaking change` label. They are kept " + "in the changelog — add the label on the issue or move the " + "entry to its regular section.\n\n" + ) + for e in anomalies_e: + f.write(f"- ⚠️ {issue_link_title(e['issue'], e['issue_title'][:80])}\n") + f.write("\n") + else: + f.write( + "✅ No anomalies found. All (issue, PR) pairs in the changelog have aligned milestone assignments.\n\n" + ) + + if n_c or n_d: + f.write("## Highlight gaps\n\n") + f.write( + "These are warnings, not anomalies: they do not affect the " + "milestone-mismatch total above. They track `:rocket:` coverage " + "across all released X.Y.0 versions. Historical entries (e.g. " + "Taiga links) predate the current reference convention and are " + "expected to appear here.\n\n" + ) + + if n_c: + f.write("### Released X.Y.0 version missing :rocket: section\n\n") + f.write( + "These released minors/majors have no `### :rocket: Epics and highlights` subsection. " + "Add highlights to help self-hosted users understand what they are missing.\n\n" + ) + for ver in anomalies_c: + f.write(f"- Version **{ver}**\n") + f.write("\n") + + if n_d: + f.write("### :rocket: entry without issue AND PR references\n\n") + f.write( + "These highlight entries lack the required issue AND PR references. " + "Add `[#ISSUE](...)` and `(PR: [#PR](...))` links.\n\n" + ) + for d in anomalies_d: + f.write(f"- **{d['version']}**: `{d['line']}`\n") + f.write("\n") + elif not (n_a or n_b or n_e): + f.write( + "✅ No highlight gaps found. All released X.Y.0 versions have properly referenced :rocket: entries.\n\n" + ) + + f.write("---\n\n") + f.write("## Context\n\n") + f.write(f"- Milestone: **{milestone}**\n") + f.write(f"- Milestone total issues (all states): {len(all_issues)}\n") + f.write( + f"- Closed issues in milestone: {sum(1 for i in all_issues if i.get('state') == 'CLOSED')}\n" + ) + f.write( + f"- Valid issues after exclusions: {len([i for i in all_issues if not issue_excluded(i)])}\n" + ) + f.write(f"- Issues referenced in changelog: {len(changelog_issues)}\n") + f.write(f"- PRs referenced in changelog: {len(changelog_prs)}\n") + + print(f"Anomaly report written to {args.output}") + + +# ───────────────────────────────────────────── +# CLI entrypoint +# ───────────────────────────────────────────── + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Helper commands for the update-changelog skill" + ) + sub = parser.add_subparsers(dest="command", required=True, title="subcommands") + + p_check = sub.add_parser( + "check-merged", help="Verify that every given PR is merged" + ) + p_check.add_argument("numbers", type=int, nargs="*", + help="PR numbers to check (space-separated)") + p_check.add_argument("--file", type=str, + help="File with one PR number per line") + p_check.add_argument("--stdin", action="store_true", + help="Read PR numbers from stdin (one per line)") + p_check.set_defaults(func=cmd_check_merged) + + p_cross = sub.add_parser( + "cross-ref", + help="Compare merged milestone PRs against the changelog section", + ) + p_cross.add_argument("milestone", help="Milestone title (e.g. '2.16.0')") + p_cross.add_argument("--changes", default="CHANGES.md", + help="Path to the changelog file (default: CHANGES.md)") + p_cross.set_defaults(func=cmd_cross_ref) + + p_report = sub.add_parser( + "report", help="Generate the anomaly report (CHANGES-ISSUES.md)" + ) + p_report.add_argument("milestone", help="Milestone title (e.g. '2.16.0')") + p_report.add_argument("--changes", default="CHANGES.md", + help="Path to the changelog file (default: CHANGES.md)") + p_report.add_argument("--output", default="CHANGES-ISSUES.md", + help="Report output path (default: CHANGES-ISSUES.md)") + p_report.set_defaults(func=cmd_report) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/scripts/gh.py b/scripts/gh.py index df4a4682e6..e0db6b4ac7 100755 --- a/scripts/gh.py +++ b/scripts/gh.py @@ -25,6 +25,7 @@ Usage: cat prs.txt | python3 scripts/gh.py prs --stdin python3 scripts/gh.py prs --milestone "2.16.0" (default: state=merged) python3 scripts/gh.py prs --milestone "2.16.0" --state all + python3 scripts/gh.py issue 11235 11236 python3 scripts/gh.py advisories (list all advisories) python3 scripts/gh.py advisories --severity critical (filter by severity) python3 scripts/gh.py advisories GHSA-xvj6-fh9w-gjw7 (single advisory detail) @@ -40,6 +41,7 @@ import json import re import subprocess import sys +import time from typing import Any @@ -47,21 +49,70 @@ REPO = "penpot/penpot" OWNER = "penpot" REPO_NAME = "penpot" +# Transient gateway timeouts from the GitHub API are retried, nothing else. +GH_RETRIES = 3 +GH_RETRY_DELAYS = (5, 15) # seconds waited before retry N (last delay repeats) + + +def run_gh_command(cmd: list[str], input_text: str | None = None) -> str: + """Run a ``gh`` command, retrying transient HTTP 504s. + + Raises `GhCommandFailed` on failure so callers can choose between + aborting (normal commands) and degrading (batched lookups). + """ + last_stderr = "" + for attempt in range(GH_RETRIES): + if attempt: + delay = GH_RETRY_DELAYS[min(attempt - 1, len(GH_RETRY_DELAYS) - 1)] + print( + f"gh: HTTP 504, retrying in {delay}s" + f" (attempt {attempt + 1}/{GH_RETRIES})...", + file=sys.stderr, + ) + time.sleep(delay) + result = subprocess.run( + cmd, input=input_text, capture_output=True, text=True + ) + if result.returncode == 0: + return result.stdout + last_stderr = result.stderr + if "504" not in result.stderr: + break + raise GhCommandFailed(last_stderr) + + +class GhCommandFailed(Exception): + """A ``gh`` invocation failed (stderr kept for the caller to judge).""" + + def __init__(self, stderr: str) -> None: + super().__init__(stderr) + self.stderr = stderr + + +def fail_gh(stderr: str) -> None: + """Report a ``gh`` failure and exit (standard behavior for CLI commands).""" + print(f"gh error: {stderr}", file=sys.stderr) + sys.exit(1) + # ───────────────────────────────────────────── # Shared helpers # ───────────────────────────────────────────── +def post_graphql(query: str, variables: dict) -> Any: + """POST a GraphQL query via ``gh`` and return the raw response body.""" + payload = json.dumps({"query": query, "variables": variables}) + stdout = run_gh_command(["gh", "api", "graphql", "--input", "-"], payload) + return json.loads(stdout) + + def run_gh_graphql(query: str, variables: dict) -> Any: """Run a GraphQL query via ``gh api graphql --input -``.""" - payload = json.dumps({"query": query, "variables": variables}) - cmd = ["gh", "api", "graphql", "--input", "-"] - result = subprocess.run(cmd, input=payload, capture_output=True, text=True) - if result.returncode != 0: - print(f"gh error: {result.stderr}", file=sys.stderr) - sys.exit(1) - body = json.loads(result.stdout) + try: + body = post_graphql(query, variables) + except GhCommandFailed as err: + fail_gh(err.stderr) if "errors" in body: for err in body["errors"]: print(f"GraphQL error: {err.get('message')}", file=sys.stderr) @@ -71,12 +122,10 @@ def run_gh_graphql(query: str, variables: dict) -> Any: def run_gh_rest(path: str) -> Any: """Run a REST API call via ``gh api``.""" - cmd = ["gh", "api", path] - result = subprocess.run(cmd, capture_output=True, text=True) - if result.returncode != 0: - print(f"gh error: {result.stderr}", file=sys.stderr) - sys.exit(1) - return json.loads(result.stdout) + try: + return json.loads(run_gh_command(["gh", "api", path])) + except GhCommandFailed as err: + fail_gh(err.stderr) # ───────────────────────────────────────────── @@ -541,6 +590,7 @@ GQL_PRS_QUERY_ITEM = """\ state mergedAt createdAt + milestone {{ title }} author {{ login }} labels(first: 20) {{ nodes {{ name }} }} closingIssuesReferences(first: 5) {{ nodes {{ number }} }} @@ -588,6 +638,7 @@ def fetch_prs_batch(pr_numbers: list[int]) -> list[dict]: "state": pr["state"], "merged_at": pr.get("mergedAt"), "created_at": pr.get("createdAt"), + "milestone": (pr.get("milestone") or {}).get("title"), "author": pr["author"]["login"] if pr["author"] else None, "labels": [lbl["name"] for lbl in pr["labels"]["nodes"]], "closing_issues": [iss["number"] for iss in pr["closingIssuesReferences"]["nodes"]], @@ -692,38 +743,19 @@ def cmd_prs(args: argparse.Namespace) -> None: print(f"Fetching {args.state} PRs via GraphQL...", file=sys.stderr) prs = fetch_milestone_prs(ms["number"], gql_states) + for pr in prs: + pr["milestone"] = ms["title"] print(f"Fetched {len(prs)} PRs total", file=sys.stderr) print(json.dumps(prs, indent=2)) return # ── Number-based path ─────────────────────────────────────────── - pr_numbers: list[int] = [] - - if args.numbers: - pr_numbers.extend(args.numbers) - - if args.file: - with open(args.file) as f: - for line in f: - line = line.strip() - if line: - pr_numbers.append(int(line)) - - if args.stdin: - for line in sys.stdin: - line = line.strip() - if line: - pr_numbers.append(int(line)) - + pr_numbers = read_numbers_from_args(args) if not pr_numbers: print("ERROR: no PR numbers provided (pass numbers, --file, --stdin, or --milestone)", file=sys.stderr) sys.exit(1) - # Deduplicate while preserving order - seen: set[int] = set() - pr_numbers = [n for n in pr_numbers if not (n in seen or seen.add(n))] - print(f"Fetching {len(pr_numbers)} PRs in batches of {PRS_BATCH_SIZE}...", file=sys.stderr) @@ -737,6 +769,159 @@ def cmd_prs(args: argparse.Namespace) -> None: print(json.dumps(all_results, indent=2)) +# ───────────────────────────────────────────── +# Subcommand: issue (fetch issues by number, batched) +# ───────────────────────────────────────────── + +GQL_ISSUE_BY_NUMBER_QUERY_ITEM = """\ + issue_{num}: issue(number: {num}) {{ + number + title + state + milestone {{ title }} + issueType {{ name }} + labels(first: 20) {{ nodes {{ name }} }} + closedByPullRequestsReferences(first: 5) {{ nodes {{ number }} }} + projectItems(first: 10) {{ + nodes {{ + project {{ title }} + fieldValueByName(name: "Status") {{ + ... on ProjectV2ItemFieldSingleSelectValue {{ + name + }} + }} + }} + }} + }} +""" + + +def issue_node_to_dict(node: dict) -> dict: + """Convert a GraphQL issue node to the shared issue dict shape.""" + issue_type = node.get("issueType") + ms = node.get("milestone") + project_status = None + for pi in (node.get("projectItems") or {}).get("nodes") or []: + project = pi.get("project") or {} + if project.get("title") == "Main": + status_field = pi.get("fieldValueByName") or {} + project_status = status_field.get("name") + break + return { + "number": node["number"], + "title": node["title"], + "state": node["state"], + "milestone": ms["title"] if ms else None, + "issue_type": issue_type["name"] if issue_type else None, + "labels": [lbl["name"] for lbl in node["labels"]["nodes"]], + "closing_prs": [pr["number"] for pr in node["closedByPullRequestsReferences"]["nodes"]], + "project_status": project_status, + } + + +class IssueBatchFailed(Exception): + """One aliased issue lookup failed; the batch cannot be trusted as a whole.""" + + +def _fetch_issues_batch(issue_numbers: list[int]) -> list[dict]: + """Single batched issue lookup; raises `IssueBatchFailed` on any failure.""" + items = "\n".join( + GQL_ISSUE_BY_NUMBER_QUERY_ITEM.format(num=n) for n in issue_numbers + ) + query = GQL_PRS_QUERY_WRAPPER.format(items=items) + variables = {"owner": OWNER, "repo": REPO_NAME} + + try: + body = post_graphql(query, variables) + except GhCommandFailed as err: + raise IssueBatchFailed(err.stderr) from err + if "errors" in body: + raise IssueBatchFailed("; ".join(e.get("message", "") for e in body["errors"])) + repo = body["data"]["repository"] + + results: list[dict] = [] + for num in issue_numbers: + node = repo.get(f"issue_{num}") + if node is None: + results.append({ + "number": num, + "error": "not_found", + }) + continue + results.append(issue_node_to_dict(node)) + return results + + +def fetch_issues_batch(issue_numbers: list[int]) -> list[dict]: + """ + Fetch details for a list of issue numbers in a single GraphQL query. + + Uses numbered aliases (issue_1234, …) so each issue is looked up by + number in one round-trip. Returns entries in the same order as the input. + + Unlike PRs (which resolve to null), a dead issue number fails the whole + batch, so when the failure names an unresolvable issue each number is + retried on its own and reported as ``not_found`` instead of aborting. + Any other failure (auth, outage, exhausted 504 retries) still aborts. + """ + try: + return _fetch_issues_batch(issue_numbers) + except IssueBatchFailed as err: + if "Could not resolve" not in str(err): + print(f"GraphQL error: {err}", file=sys.stderr) + sys.exit(1) + print(" batch lookup failed, retrying issues one by one...", + file=sys.stderr) + results: list[dict] = [] + for num in issue_numbers: + try: + results.extend(_fetch_issues_batch([num])) + except IssueBatchFailed: + results.append({"number": num, "error": "not_found"}) + return results + + +def read_numbers_from_args(args: argparse.Namespace) -> list[int]: + """Collect numbers from positional args, --file, and/or --stdin (deduplicated).""" + numbers: list[int] = [] + if args.numbers: + numbers.extend(args.numbers) + if args.file: + with open(args.file) as f: + for line in f: + line = line.strip() + if line: + numbers.append(int(line)) + if args.stdin: + for line in sys.stdin: + line = line.strip() + if line: + numbers.append(int(line)) + seen: set[int] = set() + return [n for n in numbers if not (n in seen or seen.add(n))] + + +def cmd_issue(args: argparse.Namespace) -> None: + """Handle the ``issue`` subcommand (batched by-number lookup, no filters).""" + issue_numbers = read_numbers_from_args(args) + if not issue_numbers: + print("ERROR: no issue numbers provided (pass numbers, --file, or --stdin)", + file=sys.stderr) + sys.exit(1) + + print(f"Fetching {len(issue_numbers)} issues in batches of {PRS_BATCH_SIZE}...", + file=sys.stderr) + + all_results: list[dict] = [] + for i in range(0, len(issue_numbers), PRS_BATCH_SIZE): + batch = issue_numbers[i : i + PRS_BATCH_SIZE] + print(f" batch {i // PRS_BATCH_SIZE + 1}: issues {batch[0]}..{batch[-1]}", + file=sys.stderr) + all_results.extend(fetch_issues_batch(batch)) + + print(json.dumps(all_results, indent=2)) + + # ───────────────────────────────────────────── # Subcommand: advisories # ───────────────────────────────────────────── @@ -911,6 +1096,24 @@ def main() -> None: p_link.add_argument("pr_number", type=int, help="Pull request number") p_link.set_defaults(func=cmd_link_issue) + # --- issue (by-number lookup) --- + p_issue = sub.add_parser( + "issue", help="Fetch details for one or more issues by number (batched)" + ) + p_issue.add_argument( + "numbers", type=int, nargs="*", + help="Issue numbers to fetch (space-separated)" + ) + p_issue.add_argument( + "--file", type=str, + help="File with one issue number per line" + ) + p_issue.add_argument( + "--stdin", action="store_true", + help="Read issue numbers from stdin (one per line)" + ) + p_issue.set_defaults(func=cmd_issue) + # --- advisories --- p_adv = sub.add_parser("advisories", help="List or inspect GitHub security advisories") p_adv.add_argument( From 5b02e4d8988966b150350f7a96096954a57490af Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 6 Oct 2026 16:39:01 +0000 Subject: [PATCH 037/145] :sparkles: Add find-project-anomalies skill and helper Check a milestone against the Main project board with scripts/project-anomalies.py (check writes tmp/-ANOMALIES.md): open issues with merged PRs, milestone mismatches either way, needs triage labels and unassigned non-community issues. Outsiders resolve in bulk; only a missing milestone auto-fixes, on confirmation. Teach scripts/gh.py batched issue lookup (issue subcommand), milestone on prs output, plus assignees and projects fields, and retry transient HTTP 504s. AI-assisted-by: muse-spark-1.3 --- .agents/README.md | 1 + .../skills/find-project-anomalies/SKILL.md | 116 +++++ .gitignore | 1 + .serena/memories/critical-info.md | 1 + .serena/memories/scripts/gh.md | 2 + .serena/memories/scripts/project-anomalies.md | 17 + scripts/gh.py | 73 +-- scripts/project-anomalies.py | 468 ++++++++++++++++++ scripts/test_project_anomalies.py | 278 +++++++++++ 9 files changed, 929 insertions(+), 28 deletions(-) create mode 100644 .agents/skills/find-project-anomalies/SKILL.md create mode 100644 .serena/memories/scripts/project-anomalies.md create mode 100644 scripts/project-anomalies.py create mode 100644 scripts/test_project_anomalies.py diff --git a/.agents/README.md b/.agents/README.md index 1f4e2364c5..657ab6eac3 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -508,6 +508,7 @@ JSON, REPL access, and so on. | [`ste`](skills/ste/SKILL.md) | Rewrites prose in Simplified Technical English. Loads only when you name it. | | [`refine-prompt`](skills/refine-prompt/SKILL.md) | Rewrites a rough prompt into a clearer one. Never runs the prompt. | | [`update-changelog`](skills/update-changelog/SKILL.md) | Regenerates `CHANGES.md` from a GitHub milestone. | +| [`find-project-anomalies`](skills/find-project-anomalies/SKILL.md) | Checks a milestone against the Main project board and reports anomalies to `tmp/-ANOMALIES.md`. | ### A typical round diff --git a/.agents/skills/find-project-anomalies/SKILL.md b/.agents/skills/find-project-anomalies/SKILL.md new file mode 100644 index 0000000000..208db481a9 --- /dev/null +++ b/.agents/skills/find-project-anomalies/SKILL.md @@ -0,0 +1,116 @@ +--- +name: find-project-anomalies +description: Check a GitHub milestone against the Main project board, report the five anomaly types to tmp/-ANOMALIES.md, and fix missing milestone assignments on request. +--- + +# Skill: find-project-anomalies + +Check every issue and PR in a given GitHub milestone against the Main +project board. Report the five anomaly types below, then fix the ones a +human confirms. + +## Getting the version ($ARGUMENTS) + +The milestone version comes from the skill arguments (`$ARGUMENTS`), the +same way other skills receive theirs. Accept `2.17.0`, `v2.17.0`, +`milestone 2.17.0`, or any text clearly containing one `X.Y.Z` version. + +If no version is given and none can be deduced from the arguments or the +conversation context, **stop and ask the user** which milestone to check. +Do not guess, do not default to the latest release. + +## When to Use + +- Before a release, to catch board/milestone drift while there is still time + to fix it +- After triage sessions, to verify every milestone item is tracked, owned, + and correctly assigned +- Whenever someone asks "is milestone X.Y.Z clean on Main?" + +## Prerequisites + +- `gh` CLI authenticated (`gh auth status`) +- Python 3.8+ +- `scripts/gh.py` for GitHub queries, `scripts/project-anomalies.py` for + detection and reporting (`python3 scripts/project-anomalies.py check --help`) + +## Workflow + +### 1. Resolve the version (see above), then run the check + +```bash +python3 scripts/project-anomalies.py check "2.17.0" +# explicit path: +python3 scripts/project-anomalies.py check "2.17.0" --output tmp/2.17.0-ANOMALIES.md +``` + +This fetches milestone issues and PRs in bulk (one GraphQL call per 50 +items), resolves outsiders the same way, and overwrites +`tmp/-ANOMALIES.md`. Every number renders as a full +`[#N](https://github.com/penpot/penpot/issues/N)` or +`[#N](https://github.com/penpot/penpot/pull/N)` link. + +### 2. Read the report and judge each anomaly + +The five types, in report order: + +1. **OPEN issue with a MERGED PR** — the fix landed but the issue never + closed (or it reopened). Fix: close the issue, or move it out of the + milestone if the fix did not actually land here. +2. **Milestone issue, PR elsewhere or issue off Main** — a *merged* closing + PR in another milestone (or none), or the issue itself not on the Main + board. An unmerged PR's milestone means nothing (it landed nowhere), so + it never triggers this type. Fix: align the milestones (see step 3), or + add the issue to Main. +3. **Milestone PR, issue elsewhere or off Main** — the mirror view over + *merged* PRs only: a PR released here closes an issue tracked elsewhere + (or nowhere on Main). Unmerged PRs never trigger this type — their + milestone means nothing yet. Fix: align the milestones (see step 3), or + add the issue to Main. +4. **MERGED PR on a `needs triage` issue** — landed without triage. Only a + human can triage; never auto-remove the label. +5. **MERGED PR on an unassigned, non-community issue** — no owner and no + `community contribution` label (checked on both issue and PR). Either + assign an owner or confirm it is community work. Never invent an owner. + +Scope notes (deliberate, not bugs): +- Project membership is checked **on the issue side only**. PRs are not the + unit tracked on Main, so an unprojected PR alone is never an anomaly. +- An issue with *no* milestone closed by a milestone PR **is** reported + here (type 3): unlike the changelog flow, this pairing always needs a + human look. + +### 3. Fix missing milestone assignments (only these, only on confirmation) + +The only mechanical fix in this skill is assigning a missing milestone. +The report already prints the exact command next to each such case: + +```bash +gh pr edit --milestone "" +gh issue edit --milestone "" +``` + +Rules: +- Apply **only** when one side lacks a milestone. When both sides have + *different* milestones, a human decides which one moves — never pick a + side yourself. +- Confirm **each fix (or each small batch)** with the user before running + it. Read back what the command will change. +- Everything else (closing issues, triaging, assigning owners, adding + items to the Main board) is human work: point at it, do not do it. + +### 4. Re-run until clean + +After fixes land, re-run step 1 and confirm the report shows zero +anomalies. The `tmp/` report is scratch output (gitignored): quote or +paste entries into chat when reporting back, do not commit it. + +## Key Principles + +- **No version, no run.** Stop and ask when the milestone is unknown. +- **Report first, touch nothing.** No board or milestone changes without + explicit per-item confirmation. +- **Milestone gaps auto-fix; judgment calls do not.** Only a missing + milestone is mechanical. triage, ownership, and close/reopen decisions + belong to humans. +- **Every number clickable.** The report is useless without full GitHub links. diff --git a/.gitignore b/.gitignore index 50c8d19494..a0d9844a01 100644 --- a/.gitignore +++ b/.gitignore @@ -108,5 +108,6 @@ opencode.json /.opencode/reports /.opencode/prompts /.ci-logs +/tmp/ /.codex/ /tools/__pycache__ diff --git a/.serena/memories/critical-info.md b/.serena/memories/critical-info.md index 5f1b52c81b..5e84c9834e 100644 --- a/.serena/memories/critical-info.md +++ b/.serena/memories/critical-info.md @@ -55,6 +55,7 @@ The memory is structured in a way that you can get the critical information abou - `scripts/psql` — PostgreSQL client wrapper with devenv defaults. Companion: `scripts/db-schema` for DDL dumps. See `mem:scripts/psql`. - `scripts/taiga.py` — Fetch public issues, user stories, and tasks from the Penpot Taiga project without authentication. See `mem:scripts/taiga`. - `scripts/gh.py` — GitHub operations helper: list milestone issues, fetch PR details, compare against CHANGES.md. Requires `gh` CLI. See `mem:scripts/gh`. +- `scripts/project-anomalies.py` — Main-board anomaly check for a milestone (`check` writes `tmp/-ANOMALIES.md`). Bulk resolution via `scripts/gh.py`. See `mem:scripts/project-anomalies`. - `scripts/changelog.py` — Changelog checks for the `update-changelog` skill: `check-merged` (PR merge status), `cross-ref` (milestone PRs vs changelog section), `report` (anomaly report to CHANGES-ISSUES.md). Calls `scripts/gh.py` via subprocess. See `mem:scripts/changelog`. - `scripts/error-reports.mjs` — Query error reports via RPC API with token authentication. Supports list/get operations with filtering and pagination. See `mem:scripts/error-reports`. - `scripts/clean-node-modules` — Remove stale `node_modules` from all pnpm workspaces (root, modules, member packages). Keeps the shared pnpm store at `/.pnpm-store` unless `--store`; ignores `external/` and `.opencode/`. Usage and reinstall steps: `mem:workflow/updating-pnpm`. diff --git a/.serena/memories/scripts/gh.md b/.serena/memories/scripts/gh.md index d0546e5f13..7182120524 100644 --- a/.serena/memories/scripts/gh.md +++ b/.serena/memories/scripts/gh.md @@ -49,6 +49,8 @@ python3 scripts/gh.py issues "2.16.0" --compare CHANGES.md - Issues with type "Task" are excluded (`--include-tasks` to keep them). - Issues with "Rejected" project status are excluded (`--include-rejected` to keep them). +Every issue entry carries `assignees` (logins) and `projects` (board titles); every PR entry carries `assignees`. + **Output**: JSON array to stdout; progress to stderr. ### `prs` diff --git a/.serena/memories/scripts/project-anomalies.md b/.serena/memories/scripts/project-anomalies.md new file mode 100644 index 0000000000..9d0bfe6ecc --- /dev/null +++ b/.serena/memories/scripts/project-anomalies.md @@ -0,0 +1,17 @@ +# Project anomalies helper + +`scripts/project-anomalies.py` implements the `find-project-anomalies` skill: `check ` fetches milestone issues/PRs with `--state all` via `scripts/gh.py`, resolves outsiders in bulk (one GraphQL call per 50 items), and writes `tmp/-ANOMALIES.md` (gitignored scratch output, every number a full GitHub link). + +## Anomaly types (all scoped to the milestone) + +- Open-with-merged-PR: OPEN issue with a MERGED closing PR (close it or move it out). +- Issue-PR mismatch: milestone issue whose *merged* closing PR is elsewhere/nowhere (an unmerged PR's milestone is irrelevant), or issue off the Main board. +- PR-issue mismatch: *merged* milestone PR closing an issue elsewhere/nowhere (milestone-less issues ARE reported here, unlike the changelog flow), or off the Main board. Unmerged PRs never count anywhere: their milestone is meaningless. +- Needs-triage: MERGED PR on a `needs triage` issue (human triage only, never auto-unlabel). +- Unassigned: MERGED PR on an issue with no assignees and no `community contribution` label on either side (valid states: assigned, or unassigned+community). + +## Conventions + +- Project membership is checked on the issue side only (`projects` list from `gh.py`); PRs are not the Main-tracked unit, so an unprojected PR alone is never an anomaly. +- Detection (`find_anomalies`) and rendering (`render_report`) are pure functions over pre-resolved dicts; outsiders must be merged into the lookup maps first (missing numbers are skipped, never flagged). +- The only mechanical fix is assigning a missing milestone (`gh pr/issue edit --milestone`); everything else is human judgment with per-item confirmation. diff --git a/scripts/gh.py b/scripts/gh.py index e0db6b4ac7..600c96f243 100755 --- a/scripts/gh.py +++ b/scripts/gh.py @@ -319,6 +319,7 @@ query($owner: String!, $repo: String!, $milestone: Int!, $cursor: String) { state issueType { name } labels(first: 20) { nodes { name } } + assignees(first: 10) { nodes { login } } closedByPullRequestsReferences(first: 5) { nodes { number } } projectItems(first: 10) { nodes { @@ -357,6 +358,7 @@ query($query: String!, $cursor: String) { milestone { title } issueType { name } labels(first: 20) { nodes { name } } + assignees(first: 10) { nodes { login } } closedByPullRequestsReferences(first: 5) { nodes { number } } projectItems(first: 10) { nodes { @@ -375,6 +377,32 @@ query($query: String!, $cursor: String) { """ +def node_assignees(node: dict) -> list[str]: + """Extract assignee logins from a GraphQL issue/PR node.""" + return [ + a["login"] for a in (node.get("assignees") or {}).get("nodes") or [] + ] + + +def node_projects(node: dict) -> list[str]: + """Extract project board titles from a GraphQL issue node.""" + return [ + (pi.get("project") or {}).get("title") + for pi in (node.get("projectItems") or {}).get("nodes") or [] + if (pi.get("project") or {}).get("title") + ] + + +def node_main_status(node: dict) -> str | None: + """Extract the "Main" project board status from a GraphQL issue node.""" + for pi in (node.get("projectItems") or {}).get("nodes") or []: + project = pi.get("project") or {} + if project.get("title") == "Main": + status_field = pi.get("fieldValueByName") or {} + return status_field.get("name") + return None + + def fetch_no_milestone_issues(states: str, labels: str | None = None) -> list[dict]: """ Fetch all issues that belong to NO milestone via paginated GraphQL search. @@ -384,7 +412,7 @@ def fetch_no_milestone_issues(states: str, labels: str | None = None) -> list[di labels: optional comma-separated labels to include (built into the search query) Returns: - List of {number, title, state, milestone, issue_type, labels, closing_prs, project_status} + List of {number, title, state, milestone, issue_type, labels, assignees, closing_prs, project_status, projects} """ all_nodes: list[dict] = [] cursor: str | None = None @@ -415,13 +443,6 @@ def fetch_no_milestone_issues(states: str, labels: str | None = None) -> list[di continue issue_type = node.get("issueType") ms = node.get("milestone") - project_status = None - for pi in (node.get("projectItems") or {}).get("nodes") or []: - project = pi.get("project") or {} - if project.get("title") == "Main": - status_field = pi.get("fieldValueByName") or {} - project_status = status_field.get("name") - break all_nodes.append({ "number": node["number"], "title": node["title"], @@ -429,8 +450,10 @@ def fetch_no_milestone_issues(states: str, labels: str | None = None) -> list[di "milestone": ms["title"] if ms else None, "issue_type": issue_type["name"] if issue_type else None, "labels": [lbl["name"] for lbl in node["labels"]["nodes"]], + "assignees": node_assignees(node), "closing_prs": [pr["number"] for pr in node["closedByPullRequestsReferences"]["nodes"]], - "project_status": project_status, + "project_status": node_main_status(node), + "projects": node_projects(node), }) total = len(all_nodes) @@ -452,7 +475,7 @@ def fetch_milestone_issues(milestone_num: int, states: str) -> list[dict]: states: GraphQL states enum array literal, e.g. ``"[CLOSED]"`` or ``"[OPEN CLOSED]"`` Returns: - List of {number, title, state, issue_type: str|None, labels: [str], closing_prs: [int]} + List of {number, title, state, issue_type: str|None, labels: [str], assignees: [str], closing_prs: [int], project_status, projects} """ query = GQL_ISSUES_QUERY.replace("__STATES__", states) all_nodes: list[dict] = [] @@ -473,22 +496,16 @@ def fetch_milestone_issues(milestone_num: int, states: str) -> list[dict]: if node is None: continue issue_type = node.get("issueType") - # Extract project status from the "Main" project board (if present) - project_status = None - for pi in (node.get("projectItems") or {}).get("nodes") or []: - project = pi.get("project") or {} - if project.get("title") == "Main": - status_field = pi.get("fieldValueByName") or {} - project_status = status_field.get("name") - break all_nodes.append({ "number": node["number"], "title": node["title"], "state": node["state"], "issue_type": issue_type["name"] if issue_type else None, "labels": [lbl["name"] for lbl in node["labels"]["nodes"]], + "assignees": node_assignees(node), "closing_prs": [pr["number"] for pr in node["closedByPullRequestsReferences"]["nodes"]], - "project_status": project_status, + "project_status": node_main_status(node), + "projects": node_projects(node), }) total = len(all_nodes) @@ -592,6 +609,7 @@ GQL_PRS_QUERY_ITEM = """\ createdAt milestone {{ title }} author {{ login }} + assignees(first: 10) {{ nodes {{ login }} }} labels(first: 20) {{ nodes {{ name }} }} closingIssuesReferences(first: 5) {{ nodes {{ number }} }} }} @@ -640,6 +658,7 @@ def fetch_prs_batch(pr_numbers: list[int]) -> list[dict]: "created_at": pr.get("createdAt"), "milestone": (pr.get("milestone") or {}).get("title"), "author": pr["author"]["login"] if pr["author"] else None, + "assignees": node_assignees(pr), "labels": [lbl["name"] for lbl in pr["labels"]["nodes"]], "closing_issues": [iss["number"] for iss in pr["closingIssuesReferences"]["nodes"]], }) @@ -663,6 +682,7 @@ query($owner: String!, $repo: String!, $milestone: Int!, $cursor: String) { createdAt headRefName author { login } + assignees(first: 10) { nodes { login } } labels(first: 20) { nodes { name } } files(first: 100) { nodes { path } } closingIssuesReferences(first: 5) { nodes { number } } @@ -685,7 +705,7 @@ def fetch_milestone_prs(milestone_num: int, states: str) -> list[dict]: Returns: List of {number, title, body, state, merged_at, created_at, - head_ref_name, author, labels: [str], files: [str], + head_ref_name, author, assignees, labels: [str], files: [str], closing_issues: [int]} """ query = GQL_MILESTONE_PRS_QUERY.replace("__STATES__", states) @@ -715,6 +735,7 @@ def fetch_milestone_prs(milestone_num: int, states: str) -> list[dict]: "created_at": node.get("createdAt"), "head_ref_name": node.get("headRefName"), "author": node["author"]["login"] if node["author"] else None, + "assignees": node_assignees(node), "labels": [lbl["name"] for lbl in node["labels"]["nodes"]], "files": [file["path"] for file in node["files"]["nodes"]], "closing_issues": [iss["number"] for iss in node["closingIssuesReferences"]["nodes"]], @@ -781,6 +802,7 @@ GQL_ISSUE_BY_NUMBER_QUERY_ITEM = """\ milestone {{ title }} issueType {{ name }} labels(first: 20) {{ nodes {{ name }} }} + assignees(first: 10) {{ nodes {{ login }} }} closedByPullRequestsReferences(first: 5) {{ nodes {{ number }} }} projectItems(first: 10) {{ nodes {{ @@ -800,13 +822,6 @@ def issue_node_to_dict(node: dict) -> dict: """Convert a GraphQL issue node to the shared issue dict shape.""" issue_type = node.get("issueType") ms = node.get("milestone") - project_status = None - for pi in (node.get("projectItems") or {}).get("nodes") or []: - project = pi.get("project") or {} - if project.get("title") == "Main": - status_field = pi.get("fieldValueByName") or {} - project_status = status_field.get("name") - break return { "number": node["number"], "title": node["title"], @@ -814,8 +829,10 @@ def issue_node_to_dict(node: dict) -> dict: "milestone": ms["title"] if ms else None, "issue_type": issue_type["name"] if issue_type else None, "labels": [lbl["name"] for lbl in node["labels"]["nodes"]], + "assignees": node_assignees(node), "closing_prs": [pr["number"] for pr in node["closedByPullRequestsReferences"]["nodes"]], - "project_status": project_status, + "project_status": node_main_status(node), + "projects": node_projects(node), } diff --git a/scripts/project-anomalies.py b/scripts/project-anomalies.py new file mode 100644 index 0000000000..120681234a --- /dev/null +++ b/scripts/project-anomalies.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python3 +""" +project-anomalies.py — Find anomalies on the Main project board for a milestone. + +Used by the ``find-project-anomalies`` skill. Fetches milestone issues and +PRs in bulk via ``scripts/gh.py`` (one GraphQL call per 50 items, never one +call per item) and writes a clickable report to +``tmp/-ANOMALIES.md``. + +Anomaly types (all scoped to the given milestone): + 1. open-with-merged-pr — OPEN issue with a MERGED closing PR. + 2. issue-pr-mismatch — milestone issue whose closing PR is in a different + milestone (or none), or issue not associated to the Main project. + 3. pr-issue-mismatch — milestone PR whose closed issue is in a different + milestone (or none), or not associated to the Main project. + 4. needs-triage — MERGED PR whose closed issue has the `needs triage` label. + 5. unassigned — MERGED PR whose closed issue has no assignees and no + `community contribution` label (on the issue or the PR). + +Project membership is only checked on the issue side: PRs are not the unit +tracked on the Main board, so an unprojected PR alone is never an anomaly. + +Usage: + python3 scripts/project-anomalies.py check "2.17.0" + python3 scripts/project-anomalies.py check "2.17.0" --output tmp/2.17.0-ANOMALIES.md + +Prerequisites: + - gh CLI authenticated (gh auth status) + - Python 3.8+ +""" + +import argparse +import json +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + + +REPO = "penpot/penpot" +GH_PY = Path(__file__).resolve().parent / "gh.py" +MAIN_PROJECT = "Main" +COMMUNITY_LABEL = "community contribution" +TRIAGE_LABEL = "needs triage" + + +# ───────────────────────────────────────────── +# Shared helpers +# ───────────────────────────────────────────── + + +def run_gh_py(*args: str) -> str: + """Run ``scripts/gh.py`` with the given arguments, return stdout.""" + cmd = [sys.executable, str(GH_PY), *args] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"gh.py error: {result.stderr}", file=sys.stderr) + sys.exit(1) + return result.stdout + + +def issue_url(n: int) -> str: + return f"https://github.com/{REPO}/issues/{n}" + + +def pr_url(n: int) -> str: + return f"https://github.com/{REPO}/pull/{n}" + + +def issue_link(n: int) -> str: + return f"[#{n}]({issue_url(n)})" + + +def pr_link(n: int) -> str: + return f"[#{n}]({pr_url(n)})" + + +def issue_link_title(n: int, title: str) -> str: + url = issue_url(n) + if title: + return f"[#{n}]({url}) — [{title}]({url})" + return f"[#{n}]({url})" + + +def pr_link_title(n: int, title: str) -> str: + url = pr_url(n) + if title: + return f"[#{n}]({url}) — [{title}]({url})" + return f"[#{n}]({url})" + + +def in_main_project(item: dict) -> bool: + """Whether an issue/PR dict is associated to the Main project board.""" + return MAIN_PROJECT in (item.get("projects") or []) + + +# ───────────────────────────────────────────── +# Anomaly detection (pure: no network inside) +# ───────────────────────────────────────────── + + +def find_anomalies( + milestone: str, + issues: list[dict], + prs: list[dict], + pr_by_num: dict[int, dict], + issue_by_num: dict[int, dict], +) -> dict[str, list[dict]]: + """Detect the five anomaly types for a milestone. + + Callers pre-resolve outsiders: any closing PR/issue not in the milestone + must already sit in ``pr_by_num``/``issue_by_num`` (with at least + ``milestone``, ``state``, ``labels``, ``assignees`` and ``projects``). + Unknown numbers are skipped, never flagged. + """ + t1_open_merged: list[dict] = [] + t2_issue_pr: list[dict] = [] + t3_pr_issue: list[dict] = [] + t4_needs_triage: list[dict] = [] + t5_unassigned: list[dict] = [] + + for issue in issues: + if issue.get("state") != "OPEN": + continue + merged = [ + n for n in issue.get("closing_prs", []) + if (pr_by_num.get(n) or {}).get("state") == "MERGED" + ] + if merged: + t1_open_merged.append({ + "issue": issue["number"], + "issue_title": issue.get("title", ""), + "prs": sorted(merged), + }) + + for issue in issues: + for pr_num in issue.get("closing_prs", []): + pr = pr_by_num.get(pr_num) + if pr is None: + continue + # An unmerged PR has landed nowhere: its milestone says nothing + # about any release, so only merged PRs count here. + if pr.get("state") != "MERGED": + continue + if pr.get("milestone") != milestone: + t2_issue_pr.append({ + "issue": issue["number"], + "issue_title": issue.get("title", ""), + "pr": pr_num, + "pr_milestone": pr.get("milestone"), + }) + if not in_main_project(issue): + t2_issue_pr.append({ + "issue": issue["number"], + "issue_title": issue.get("title", ""), + "pr": None, + "pr_milestone": None, + "projects": issue.get("projects") or [], + }) + + for pr in prs: + # Like type 2: only a merged PR has landed somewhere, so only + # merged PRs take part in milestone pairing. (Whether the issue is + # on the Main board is checked regardless.) + if pr.get("state") != "MERGED": + continue + for issue_num in pr.get("closing_issues", []): + issue = issue_by_num.get(issue_num) + if issue is None: + continue + if issue.get("milestone", milestone) != milestone or not in_main_project(issue): + t3_pr_issue.append({ + "pr": pr["number"], + "pr_title": pr.get("title", ""), + "issue": issue_num, + "issue_title": issue.get("title", ""), + "issue_milestone": issue.get("milestone", milestone), + "projects": issue.get("projects") or [], + }) + + for pr in prs: + if pr.get("state") != "MERGED": + continue + for issue_num in pr.get("closing_issues", []): + issue = issue_by_num.get(issue_num) + if issue is None: + continue + if TRIAGE_LABEL in (issue.get("labels") or []): + t4_needs_triage.append({ + "pr": pr["number"], + "pr_title": pr.get("title", ""), + "issue": issue_num, + "issue_title": issue.get("title", ""), + }) + if not issue.get("assignees") and COMMUNITY_LABEL not in ( + (issue.get("labels") or []) + (pr.get("labels") or []) + ): + t5_unassigned.append({ + "pr": pr["number"], + "pr_title": pr.get("title", ""), + "issue": issue_num, + "issue_title": issue.get("title", ""), + }) + + return { + "open_merged": t1_open_merged, + "issue_pr": t2_issue_pr, + "pr_issue": t3_pr_issue, + "needs_triage": t4_needs_triage, + "unassigned": t5_unassigned, + } + + +# ───────────────────────────────────────────── +# Report rendering (pure) +# ───────────────────────────────────────────── + + +def milestone_fix(kind: str, number: int, milestone: str) -> str: + """Render the one-line `gh` command that assigns a missing milestone.""" + return f"Fix: `gh {kind} edit {number} --milestone \"{milestone}\"`" + + +def render_report( + milestone: str, + anomalies: dict[str, list[dict]], + total_issues: int, + closed_issues: int, + total_prs: int, + merged_prs: int, +) -> str: + """Render the full anomalies report as Markdown.""" + t1 = anomalies["open_merged"] + t2 = anomalies["issue_pr"] + t3 = anomalies["pr_issue"] + t4 = anomalies["needs_triage"] + t5 = anomalies["unassigned"] + total = len(t1) + len(t2) + len(t3) + len(t4) + len(t5) + + lines = [ + f"# Project Anomalies — {milestone}", + "", + f"Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", + "", + "---", + "", + "## Summary", + "", + f"- **OPEN issue with a MERGED PR:** {len(t1)}", + f"- **Milestone issue, PR elsewhere/unprojected:** {len(t2)}", + f"- **Milestone PR, issue elsewhere/unprojected:** {len(t3)}", + f"- **MERGED PR on a `needs triage` issue:** {len(t4)}", + f"- **MERGED PR on an unassigned, non-community issue:** {len(t5)}", + f"- **Total anomalies:** {total}", + "", + ] + + if total == 0: + lines += [ + "✅ No anomalies found. The milestone and the Main project board agree.", + "", + ] + else: + lines += ["## Anomalies", ""] + if t1: + lines += [ + "### OPEN issue with a MERGED PR", + "", + "The fix already landed but the issue never closed (or it was " + "reopened). Close the issue or move it out of the milestone.", + "", + ] + for e in t1: + lines.append(f"- {issue_link_title(e['issue'], e['issue_title'][:80])}") + for n in e["prs"]: + lines.append(f" - 🔴 Merged {pr_link(n)}") + lines.append("") + + if t2: + lines += [ + "### Milestone issue, PR elsewhere or issue off the Main board", + "", + ] + by_issue: dict[int, list[dict]] = {} + for e in t2: + by_issue.setdefault(e["issue"], []).append(e) + for issue_num in sorted(by_issue): + entries = by_issue[issue_num] + lines.append( + f"- {issue_link_title(issue_num, entries[0]['issue_title'][:80])}" + ) + for e in entries: + if e["pr"] is None: + projects = ", ".join(e["projects"]) or "_none_" + lines.append( + " - 🔴 Issue is not on the Main project board " + f"(projects: {projects})" + ) + else: + ms = e["pr_milestone"] or "_none_" + badge = "🔴" if e["pr_milestone"] is None else "⚠️" + lines.append( + f" - {badge} Closing {pr_link(e['pr'])} is in milestone " + f"**{ms}** (expected: {milestone})" + ) + if e["pr_milestone"] is None: + lines.append( + f" {milestone_fix('pr', e['pr'], milestone)}" + ) + lines.append("") + + if t3: + lines += [ + "### Milestone PR, issue elsewhere or off the Main board", + "", + ] + by_pr: dict[int, list[dict]] = {} + for e in t3: + by_pr.setdefault(e["pr"], []).append(e) + for pr_num in sorted(by_pr): + entries = by_pr[pr_num] + lines.append( + f"- {pr_link_title(pr_num, entries[0]['pr_title'][:80])}" + ) + for e in entries: + ms = e["issue_milestone"] or "_none_" + if e["issue_milestone"] != milestone: + lines.append( + f" - ⚠️ Closing {issue_link(e['issue'])} is in milestone " + f"**{ms}** (expected: {milestone})" + ) + if e["issue_milestone"] is None: + lines.append( + f" {milestone_fix('issue', e['issue'], milestone)}" + ) + else: + projects = ", ".join(e["projects"]) or "_none_" + lines.append( + f" - 🔴 Closing {issue_link(e['issue'])} is not on the " + f"Main project board (projects: {projects})" + ) + lines.append("") + + if t4: + lines += [ + "### MERGED PR on a `needs triage` issue", + "", + "The fix landed but the issue was never triaged. Human triage needed.", + "", + ] + for e in t4: + lines.append( + f"- ⚠️ {pr_link_title(e['pr'], e['pr_title'][:80])}" + f" closes {issue_link(e['issue'])}" + ) + lines.append("") + + if t5: + lines += [ + "### MERGED PR on an unassigned, non-community issue", + "", + "No owner and no community label. Assign an owner or confirm it " + "is a community contribution.", + "", + ] + for e in t5: + lines.append( + f"- ⚠️ {pr_link_title(e['pr'], e['pr_title'][:80])}" + f" closes {issue_link(e['issue'])}" + ) + lines.append("") + + lines += [ + "---", + "", + "## Context", + "", + f"- Milestone: **{milestone}**", + f"- Milestone issues (all states): {total_issues}", + f"- Open issues in milestone: {total_issues - closed_issues}", + f"- Milestone PRs (all states): {total_prs}", + f"- Merged PRs in milestone: {merged_prs}", + "", + ] + return "\n".join(lines) + + +# ───────────────────────────────────────────── +# CLI entrypoint +# ───────────────────────────────────────────── + + +def cmd_check(args: argparse.Namespace) -> None: + """Fetch milestone data in bulk, detect anomalies, write the report.""" + milestone = args.milestone + + all_issues = json.loads(run_gh_py("issues", milestone, "--state", "all")) + issue_by_num = {i["number"]: i for i in all_issues} + + all_prs = json.loads(run_gh_py("prs", "--milestone", milestone, "--state", "all")) + pr_by_num = {p["number"]: p for p in all_prs} + + # Resolve outsiders in bulk (one GraphQL call per 50 items). + unknown_prs = set() + for issue in all_issues: + unknown_prs.update(issue.get("closing_prs", [])) + unknown_prs -= set(pr_by_num) + if unknown_prs: + print( + f"Resolving {len(unknown_prs)} PRs outside {milestone}...", + file=sys.stderr, + ) + for pr in json.loads(run_gh_py("prs", *[str(n) for n in sorted(unknown_prs)])): + pr_by_num[pr["number"]] = pr + + unknown_issues = set() + for pr in all_prs: + unknown_issues.update(pr.get("closing_issues", [])) + unknown_issues -= set(issue_by_num) + if unknown_issues: + print( + f"Resolving {len(unknown_issues)} issues outside {milestone}...", + file=sys.stderr, + ) + for issue in json.loads( + run_gh_py("issue", *[str(n) for n in sorted(unknown_issues)]) + ): + if "error" not in issue: + issue_by_num[issue["number"]] = issue + + anomalies = find_anomalies(milestone, all_issues, all_prs, pr_by_num, issue_by_num) + report = render_report( + milestone, + anomalies, + total_issues=len(all_issues), + closed_issues=sum(1 for i in all_issues if i.get("state") == "CLOSED"), + total_prs=len(all_prs), + merged_prs=sum(1 for p in all_prs if p.get("state") == "MERGED"), + ) + + output = Path(args.output or f"tmp/{milestone}-ANOMALIES.md") + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(report) + total = sum(len(v) for v in anomalies.values()) + print(f"{total} anomalies written to {output}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Find Main-project anomalies for a milestone" + ) + sub = parser.add_subparsers(dest="command", required=True, title="subcommands") + + p_check = sub.add_parser("check", help="Detect anomalies and write the report") + p_check.add_argument("milestone", help="Milestone title (e.g. '2.17.0')") + p_check.add_argument( + "--output", + default=None, + help="Report path (default: tmp/-ANOMALIES.md)", + ) + p_check.set_defaults(func=cmd_check) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_project_anomalies.py b/scripts/test_project_anomalies.py new file mode 100644 index 0000000000..ca781c58d2 --- /dev/null +++ b/scripts/test_project_anomalies.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +"""Tests for scripts/project-anomalies.py (pure logic, no network). + +Run with: + + python3 scripts/test_project_anomalies.py +""" + +import importlib.machinery +import importlib.util +import pathlib +import sys +import unittest + + +# Loading scripts/project-anomalies.py should not emit scripts/__pycache__/. +sys.dont_write_bytecode = True + +SCRIPT_PATH = pathlib.Path(__file__).resolve().parent / "project-anomalies.py" + + +def load_pa(): + """Load scripts/project-anomalies.py as a module without running its CLI.""" + loader = importlib.machinery.SourceFileLoader("project_anomalies", str(SCRIPT_PATH)) + spec = importlib.util.spec_from_loader("project_anomalies", loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +pa = load_pa() + +M = "2.17.0" + + +def issue(n, **kw): + base = { + "number": n, + "title": f"issue {n}", + "state": "CLOSED", + "milestone": M, + "labels": [], + "assignees": ["someone"], + "closing_prs": [], + "project_status": "Done", + "projects": ["Main"], + } + base.update(kw) + return base + + +def pr(n, **kw): + base = { + "number": n, + "title": f"pr {n}", + "state": "MERGED", + "milestone": M, + "labels": [], + "assignees": ["someone"], + "closing_issues": [], + } + base.update(kw) + return base + + +def run(issues, prs): + return pa.find_anomalies( + M, issues, prs, + {p["number"]: p for p in prs}, + {i["number"]: i for i in issues}, + ) + + +class OpenMergedTests(unittest.TestCase): + def test_open_issue_with_merged_pr_is_flagged(self): + out = run( + [issue(1, state="OPEN", closing_prs=[11])], + [pr(11, closing_issues=[1])], + ) + self.assertEqual(len(out["open_merged"]), 1) + self.assertEqual(out["open_merged"][0]["prs"], [11]) + + def test_open_issue_with_unmerged_pr_is_clean(self): + out = run( + [issue(1, state="OPEN", closing_prs=[11])], + [pr(11, state="OPEN", closing_issues=[1])], + ) + self.assertEqual(out["open_merged"], []) + + def test_closed_issue_with_merged_pr_is_clean(self): + out = run( + [issue(1, closing_prs=[11])], + [pr(11, closing_issues=[1])], + ) + self.assertTrue(all(v == [] for v in out.values())) + + +class IssuePrMismatchTests(unittest.TestCase): + def test_pr_in_other_milestone_is_flagged(self): + out = run( + [issue(1, closing_prs=[11])], + [pr(11, milestone="2.18.0")], + ) + self.assertEqual(len(out["issue_pr"]), 1) + self.assertEqual(out["issue_pr"][0]["pr_milestone"], "2.18.0") + + def test_pr_without_milestone_is_flagged(self): + out = run( + [issue(1, closing_prs=[11])], + [pr(11, milestone=None)], + ) + self.assertEqual(len(out["issue_pr"]), 1) + + def test_unmerged_pr_milestone_is_ignored(self): + # An OPEN (unmerged) PR has landed nowhere: its milestone is + # irrelevant, even when it differs. + out = run( + [issue(1, closing_prs=[11])], + [pr(11, state="OPEN", milestone="2.18.0")], + ) + self.assertEqual(out["issue_pr"], []) + + def test_off_board_issue_flagged_despite_unmerged_pr(self): + # The project check does not depend on PR merge state. + out = run( + [issue(1, projects=["Other"], closing_prs=[11])], + [pr(11, state="OPEN", milestone="2.18.0")], + ) + self.assertEqual(len(out["issue_pr"]), 1) + self.assertIsNone(out["issue_pr"][0]["pr"]) + + def test_issue_off_main_board_is_flagged_without_pr(self): + out = run([issue(1, projects=["Other"], project_status=None)], []) + self.assertEqual(len(out["issue_pr"]), 1) + self.assertIsNone(out["issue_pr"][0]["pr"]) + + def test_aligned_pair_is_clean(self): + out = run( + [issue(1, closing_prs=[11])], + [pr(11, closing_issues=[1])], + ) + self.assertEqual(out["issue_pr"], []) + self.assertEqual(out["pr_issue"], []) + + +class PrIssueMismatchTests(unittest.TestCase): + def test_issue_in_other_milestone_is_flagged(self): + other = issue(2, milestone="2.16.0") + out = pa.find_anomalies( + M, [issue(1)], [pr(11, closing_issues=[2])], + {11: pr(11, closing_issues=[2])}, {1: issue(1), 2: other}, + ) + self.assertEqual(len(out["pr_issue"]), 1) + + def test_issue_without_milestone_is_flagged(self): + # Unlike the changelog flow, here a milestone-less issue closed by a + # milestone PR is reported: the pairing needs human judgment. + other = issue(2, milestone=None, projects=[], project_status=None) + out = pa.find_anomalies( + M, [issue(1)], [pr(11, closing_issues=[2])], + {11: pr(11, closing_issues=[2])}, {1: issue(1), 2: other}, + ) + self.assertEqual(len(out["pr_issue"]), 1) + + def test_issue_off_main_board_is_flagged(self): + other = issue(2, projects=["Other"], project_status=None) + out = pa.find_anomalies( + M, [issue(1)], [pr(11, closing_issues=[2])], + {11: pr(11, closing_issues=[2])}, {1: issue(1), 2: other}, + ) + self.assertEqual(len(out["pr_issue"]), 1) + + def test_milestone_issue_keeps_milestone_in_record(self): + # Milestone issues carry no "milestone" key (known by construction); + # an off-board one must still render its own milestone, never _none_. + mine = issue(1, projects=["Other"], project_status=None) + del mine["milestone"] + out = pa.find_anomalies( + M, [mine], [pr(11, closing_issues=[1])], + {11: pr(11, closing_issues=[1])}, {1: mine}, + ) + self.assertEqual(len(out["pr_issue"]), 1) + self.assertEqual(out["pr_issue"][0]["issue_milestone"], M) + report = pa.render_report(M, out, 1, 0, 1, 1) + self.assertNotIn("gh issue edit 1", report) + + def test_unmerged_pr_is_ignored(self): + # Closed-unmerged (or open) milestone PRs take no part in milestone + # pairing: only merged PRs landed somewhere. + other = issue(2, milestone="2.16.0") + for state in ("OPEN", "CLOSED"): + out = pa.find_anomalies( + M, [issue(1)], [pr(11, state=state, closing_issues=[2])], + {11: pr(11, state=state, closing_issues=[2])}, + {1: issue(1), 2: other}, + ) + self.assertEqual(out["pr_issue"], [], f"state={state}") + + +class NeedsTriageTests(unittest.TestCase): + def test_merged_pr_on_triaged_issue_is_clean(self): + out = run( + [issue(1, labels=["bug"])], + [pr(11, closing_issues=[1])], + ) + self.assertEqual(out["needs_triage"], []) + + def test_merged_pr_on_needs_triage_issue_is_flagged(self): + out = run( + [issue(1, labels=["needs triage"])], + [pr(11, closing_issues=[1])], + ) + self.assertEqual(len(out["needs_triage"]), 1) + + def test_unmerged_pr_on_needs_triage_issue_is_clean(self): + out = run( + [issue(1, labels=["needs triage"])], + [pr(11, state="OPEN", closing_issues=[1])], + ) + self.assertEqual(out["needs_triage"], []) + + +class UnassignedTests(unittest.TestCase): + def test_assigned_issue_is_clean(self): + out = run( + [issue(1, assignees=["owner"])], + [pr(11, closing_issues=[1])], + ) + self.assertEqual(out["unassigned"], []) + + def test_unassigned_community_issue_is_clean(self): + out = run( + [issue(1, assignees=[], labels=["community contribution"])], + [pr(11, closing_issues=[1])], + ) + self.assertEqual(out["unassigned"], []) + + def test_unassigned_community_pr_is_clean(self): + out = run( + [issue(1, assignees=[])], + [pr(11, closing_issues=[1], labels=["community contribution"])], + ) + self.assertEqual(out["unassigned"], []) + + def test_unassigned_non_community_issue_is_flagged(self): + out = run( + [issue(1, assignees=[])], + [pr(11, closing_issues=[1])], + ) + self.assertEqual(len(out["unassigned"]), 1) + + def test_unmerged_pr_is_clean(self): + out = run( + [issue(1, assignees=[])], + [pr(11, state="CLOSED", closing_issues=[1])], + ) + self.assertEqual(out["unassigned"], []) + + +class RenderTests(unittest.TestCase): + def test_clean_report_says_so(self): + out = run([issue(1, closing_prs=[11])], [pr(11, closing_issues=[1])]) + report = pa.render_report(M, out, 1, 0, 1, 1) + self.assertIn("Total anomalies:** 0", report) + self.assertIn("✅ No anomalies", report) + + def test_every_number_is_a_clickable_link(self): + out = run( + [issue(1, state="OPEN", closing_prs=[11])], + [pr(11, closing_issues=[1])], + ) + report = pa.render_report(M, out, 1, 0, 1, 1) + self.assertIn("[#1](https://github.com/penpot/penpot/issues/1)", report) + self.assertIn("[#11](https://github.com/penpot/penpot/pull/11)", report) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From e7041234f37cf72863a048f845d82024ff5bd4a1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 7 Oct 2026 08:49:32 +0200 Subject: [PATCH 038/145] :paperclip: Set library (sdk) version to 1.2.0 --- .gitignore | 2 ++ library/CHANGES.md | 4 ++++ library/package.json | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a0d9844a01..707d430b13 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,7 @@ opencode.json /web /library/target/ /library/*.zip +/library/.shadow-cljs /external /penpot-nitrate /test-results/ @@ -89,6 +90,7 @@ opencode.json /blob-report/ /playwright/.cache/ /render-wasm/target/ +/render-wasm/.render-wasm-build.lock /media-processor/dist/ /**/node_modules /**/.yarn/* diff --git a/library/CHANGES.md b/library/CHANGES.md index ac3e4ca0bd..b1f7acdc70 100644 --- a/library/CHANGES.md +++ b/library/CHANGES.md @@ -1,5 +1,9 @@ # CHANGELOG +## 1.2.0 + +- Update deps and build it with penpot 2.18.3 + ## 1.2.0-RC1 diff --git a/library/package.json b/library/package.json index dec0f1bdc6..deb8096254 100644 --- a/library/package.json +++ b/library/package.json @@ -1,8 +1,8 @@ { "name": "@penpot/library", - "version": "1.2.0-RC1", + "version": "1.2.0", "license": "MPL-2.0", - "author": "Kaleidos INC Sucursal en España SL", + "author": "KALEIDOS SUBSIDIARY SL", "packageManager": "pnpm@12.5.1+sha512.e3f305bc784a2bc89f5ad3b6138889470fae8d2af5f36b61216ec91c2c3d64089775f9de38aac331044ea40f245cb0d5666392dfdf65824e1907ef6a2c62de5f", "type": "module", "repository": { From 2f47be6c155ed0301bdfba12db78df94ac0b7944 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 7 Oct 2026 09:18:57 +0200 Subject: [PATCH 039/145] :bug: Show the size badge on paths and straight lines (#12066) * :bug: Show the size badge on paths and straight lines The badge was hidden when the shape was smaller than the badge in either side. Flat paths have a zero width or height, so they never showed it, and straight two-point paths hid it on purpose. Paths now only compare their longest side with the badge width. A straight line compares its length, and the badge runs along the line, rotated with it and kept upright. Near the bottom of the viewport the badge moves to the other side of the line. The badge now reads the size from the shape :selrect, like the measures inputs, so a rotated path shows the same values in both places. The three badge branches share a single render block. AI-assisted-by: claude-opus-5-5 * :recycle: Extract the size badge layout and add tests for it Move the badge geometry out of selection-size-badge* into the pure size-badge-layout function, so it can be tested without rendering. The component now only picks the color and draws the result. Add unit tests for rects, flat paths and straight lines: when the badge hides, where it sits, how it rotates and how it moves when it reaches the bottom of the viewport. Keep the line rotation between -90 and 90 degrees instead of returning 360 for lines drawn leftward. The e2e test still expected straight lines to hide the badge. It now checks that the badge shows and follows the line angle. AI-assisted-by: claude-opus-5-5 * :sparkles: Show the length of straight lines in the size badge A straight two-point path is measured by its length, not by the box around it, so its size badge now shows the length with two decimals at most instead of width x height. Other shapes and paths keep width x height. The shorter text makes the badge narrower, so it also shows on shorter lines; the rule is the same, the badge needs the line to be at least as long as the badge. Relates to #12064 AI-assisted-by: claude-opus-5-5 * :bug: Hide the path size badge when the path is lower than it Paths other than a straight line only hid the size badge when their longest side was narrower than it, so a wide but low path, or a narrow but tall one, still showed it. Flat paths no longer need that special case, since straight lines get their own length badge. Use the same rule as any other shape: hide the badge when either the width or the height of the path is smaller than the badge. AI-assisted-by: claude-opus-5-5 --- .../playwright/ui/specs/workspace.spec.js | 19 +- frontend/src/app/main/ui/measurements.cljs | 183 ++++++++++-------- frontend/test/frontend_tests/runner.cljs | 2 + .../frontend_tests/ui/measurements_test.cljs | 143 ++++++++++++++ 4 files changed, 265 insertions(+), 82 deletions(-) create mode 100644 frontend/test/frontend_tests/ui/measurements_test.cljs diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js index ee3400079b..051530b877 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -171,8 +171,8 @@ test("Selection size badge shows dimensions for path shapes", async ({ page }) = // Workaround: hover viewport first to avoid nil mouse position crash await workspacePage.viewport.hover(); - // Draw a path with two segments; a single straight segment shows - // endpoint controls instead of the size badge + // Draw a path with two segments; a single straight segment places + // the size badge along the line instead await workspacePage.pathButton.click(); await workspacePage.clickAt(779, 163); await workspacePage.clickAt(951, 258); @@ -189,7 +189,7 @@ test("Selection size badge shows dimensions for path shapes", async ({ page }) = await expect(badgeText).toHaveText(/\d+\.?\d* x \d+\.?\d*/); }); -test("Selection size badge is hidden for straight line paths", async ({ +test("Selection size badge runs along straight line paths", async ({ page, }) => { const workspacePage = new WasmWorkspacePage(page); @@ -204,7 +204,7 @@ test("Selection size badge is hidden for straight line paths", async ({ // Workaround: hover viewport first to avoid nil mouse position crash await workspacePage.viewport.hover(); - // Draw a path with a single straight segment + // Draw a path with a single straight segment, going down to the right await workspacePage.pathButton.click(); await workspacePage.clickAt(779, 163); await workspacePage.clickAt(951, 258); @@ -216,7 +216,16 @@ test("Selection size badge is hidden for straight line paths", async ({ await page.keyboard.press("Escape"); await expect(page.locator(".line-controls")).toBeVisible(); - await expect(page.locator(".selection-size-badge")).toHaveCount(0); + + // A straight line shows its length, with two decimals at most + const badge = page.locator(".selection-size-badge"); + await expect(badge.locator("text")).toHaveText(/^\d+(\.\d{1,2})?$/); + + // The badge follows the line angle, about 29 degrees + const transform = await badge.getAttribute("transform"); + const rotation = Number(transform.match(/rotate\(([^)]+)\)/)[1]); + expect(rotation).toBeGreaterThan(20); + expect(rotation).toBeLessThan(40); }); test("User makes a group", async ({ page }) => { diff --git a/frontend/src/app/main/ui/measurements.cljs b/frontend/src/app/main/ui/measurements.cljs index b722bf8e1c..17c2d37a28 100644 --- a/frontend/src/app/main/ui/measurements.cljs +++ b/frontend/src/app/main/ui/measurements.cljs @@ -16,6 +16,7 @@ [app.common.math :as mth] [app.common.types.component :as ctk] [app.common.types.path :as path] + [app.common.types.path.helpers :as path.helpers] [app.common.uuid :as uuid] [app.main.constants :as mconst] [app.main.ui.formats :as fmt] @@ -239,28 +240,41 @@ (<= (+ cy (/ badge-height 2)) (+ (:y vbox) (:height vbox))))) -(mf/defc selection-size-badge* - [{:keys [zoom shapes vbox]}] +(defn- line-badge-position + "Badge center and rotation for a badge running along the segment `p1`-`p2`. + The direction is flipped when needed so the text stays upright." + [p1 p2 offset] + (let [dir (gpt/subtract p2 p1) + angle (gpt/angle dir) + upright? (and (> angle -90) (<= angle 90)) + dir (if upright? dir (gpt/negate dir)) + rot (cond + upright? angle + (pos? angle) (- angle 180) + :else (+ angle 180)) + mid-point (gpt/lerp p1 p2 0.5) + normal (gpt/normal-left dir)] + {:cx (+ (:x mid-point) (* (:x normal) offset)) + :cy (+ (:y mid-point) (* (:y normal) offset)) + :rot rot})) + +(defn size-badge-layout + "Geometry of the size badge for the selected `shapes`, in viewport + units. Returns nil when the badge is hidden, otherwise a map with the + badge `:text`, `:width`, `:height`, its center `:cx`/`:cy` and its + rotation `:rot` in degrees." + [shapes zoom vbox] (let [badge-height (/ selection-badge-height zoom) badge-padding-x (/ selection-badge-padding-x zoom) badge-gap (/ selection-badge-vertical-gap zoom) - badge-radius (/ selection-badge-border-radius zoom) badge-char-width (/ selection-badge-char-width zoom) single-shape (and (= (count shapes) 1) (first shapes)) - ;; Straight paths use endpoint controls instead of a size badge. single-line? (and single-shape (cfh/path-shape? single-shape) (path/single-line? (dm/get-prop single-shape :content))) - component-color? (if single-shape - (ctk/instance-head? single-shape) - (every? ctk/instance-head? shapes)) - badge-bg-color (if component-color? - selection-badge-bg-color-component - selection-badge-bg-color) - rotation (when single-shape (dm/get-prop single-shape :rotation)) has-rotation? (and rotation (not (mth/almost-zero? rotation))) @@ -269,86 +283,101 @@ ;; including component instances and shapes with transforms. selrect (gsh/shapes->rect shapes) - ;; For single shapes we show the original dimensions, - ;; for multiple shapes show the bounding box - shape-width (if single-shape - (or (dm/get-prop single-shape :width) - (:width selrect)) - (:width selrect)) - shape-height (if single-shape - (or (dm/get-prop single-shape :height) - (:height selrect)) - (:height selrect)) + ;; For single shapes we show the same dimensions as the measures + ;; inputs (the shape :selrect, which ignores rotation and also + ;; exists for paths); for multiple shapes show the bounding box + shape-rect (if single-shape + (dm/get-prop single-shape :selrect) + selrect) + shape-width (:width shape-rect) + shape-height (:height shape-rect) - text (dm/str (fmt/format-number shape-width) " x " (fmt/format-number shape-height)) + [line-p1 line-p2] + (when single-line? + (let [content (dm/get-prop single-shape :content)] + [(path.helpers/segment->point (nth content 0)) + (path.helpers/segment->point (nth content 1))])) + + line-length (when single-line? + (gpt/distance line-p1 line-p2)) + + ;; A straight line shows its length (two decimals at most); + ;; other shapes show their width and height. + text (if single-line? + (fmt/format-number line-length) + (dm/str (fmt/format-number shape-width) " x " (fmt/format-number shape-height))) text-width (* (count text) badge-char-width) badge-width (+ text-width (* 2 badge-padding-x)) + offset (+ badge-gap (/ badge-height 2)) - hidden? (or single-line? - (< shape-width badge-width) - (< shape-height badge-height))] + ;; A straight line only has a length; any other shape, paths + ;; included, must fit the badge in both dimensions. + hidden? (if single-line? + (< line-length badge-width) + (or (< shape-width badge-width) + (< shape-height badge-height)))] (when-not ^boolean hidden? - (if has-rotation? - (let [edge (get-edge-for-badge rotation) - points (dm/get-prop single-shape :points) - offset (+ badge-gap (/ badge-height 2)) + (-> (cond + single-line? + (let [position (line-badge-position line-p1 line-p2 offset)] + (if (fits-above-vbox-bottom? (:cy position) badge-height vbox) + position + ;; Same rotation, opposite side of the line. + (let [flipped (line-badge-position line-p1 line-p2 (- offset))] + (assoc flipped :rot (:rot position))))) + has-rotation? + (let [edge (get-edge-for-badge rotation) + points (dm/get-prop single-shape :points) + position (edge-badge-position points edge offset) + position (if (fits-above-vbox-bottom? (:cy position) badge-height vbox) + position + (edge-badge-position points (opposite-edge edge) offset))] ;; Rotation follows the original edge even if the position ;; flips to the opposite one for lack of space, so the text ;; stays upright. - badge-rot (+ rotation (edge-rot-offset edge)) + (assoc position :rot (+ rotation (edge-rot-offset edge)))) - position (edge-badge-position points edge offset) - position (if (fits-above-vbox-bottom? (:cy position) badge-height vbox) - position - (edge-badge-position points (opposite-edge edge) offset)) + :else + (let [below-cy (+ (:y selrect) (:height selrect) offset)] + {:cx (+ (:x selrect) (/ (:width selrect) 2)) + :cy (if (fits-above-vbox-bottom? below-cy badge-height vbox) + below-cy + (- (:y selrect) offset)) + :rot 0})) + (assoc :text text + :width badge-width + :height badge-height))))) - badge-x (- (/ badge-width 2)) - badge-y (- (/ badge-height 2)) - badge-cx (:cx position) - badge-cy (:cy position)] +(mf/defc selection-size-badge* + [{:keys [zoom shapes vbox]}] + (let [single-shape (and (= (count shapes) 1) (first shapes)) + component-color? (if single-shape + (ctk/instance-head? single-shape) + (every? ctk/instance-head? shapes)) + badge-bg-color (if component-color? + selection-badge-bg-color-component + selection-badge-bg-color) + badge-radius (/ selection-badge-border-radius zoom)] - [:g.selection-size-badge {:pointer-events "none" - :transform (dm/str "translate(" badge-cx "," badge-cy ") rotate(" badge-rot ")")} - [:rect {:x badge-x - :y badge-y - :width badge-width - :height badge-height - :rx badge-radius - :ry badge-radius - :style {:fill badge-bg-color}}] - [:text {:class (stl/css :badge-text) - :x 0 - :y 0 - :text-anchor "middle" - :dominant-baseline "middle"} - text]]) - - (let [badge-x (- (/ badge-width 2)) - badge-y (- (/ badge-height 2)) - badge-cx (+ (:x selrect) (/ (:width selrect) 2)) - below-cy (+ (:y selrect) (:height selrect) badge-gap (/ badge-height 2)) - badge-cy (if (fits-above-vbox-bottom? below-cy badge-height vbox) - below-cy - (- (:y selrect) badge-gap (/ badge-height 2)))] - - [:g.selection-size-badge {:pointer-events "none" - :transform (dm/str "translate(" badge-cx "," badge-cy ")")} - [:rect {:x badge-x - :y badge-y - :width badge-width - :height badge-height - :rx badge-radius - :ry badge-radius - :style {:fill badge-bg-color}}] - [:text {:class (stl/css :badge-text) - :x 0 - :y 0 - :text-anchor "middle" - :dominant-baseline "middle"} - text]]))))) + (when-let [{:keys [text width height cx cy rot]} (size-badge-layout shapes zoom vbox)] + [:g.selection-size-badge {:pointer-events "none" + :transform (dm/str "translate(" cx "," cy ") rotate(" rot ")")} + [:rect {:x (- (/ width 2)) + :y (- (/ height 2)) + :width width + :height height + :rx badge-radius + :ry badge-radius + :style {:fill badge-bg-color}}] + [:text {:class (stl/css :badge-text) + :x 0 + :y 0 + :text-anchor "middle" + :dominant-baseline "middle"} + text]]))) (mf/defc distance-display* [{:keys [from to zoom bounds]}] (let [fixed-x (if (gsh/fully-contained? from to) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 865303404e..7a7da64b1a 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -108,6 +108,7 @@ [frontend-tests.ui.history-test] [frontend-tests.ui.inspect-stroke-tokens-test] [frontend-tests.ui.layout-container-multiple-test] + [frontend-tests.ui.measurements-test] [frontend-tests.ui.measures-menu-props-test] [frontend-tests.ui.organization-team-switch-test] [frontend-tests.ui.routes-test] @@ -242,6 +243,7 @@ 'frontend-tests.ui.history-test 'frontend-tests.ui.inspect-stroke-tokens-test 'frontend-tests.ui.layout-container-multiple-test + 'frontend-tests.ui.measurements-test 'frontend-tests.ui.measures-menu-props-test 'frontend-tests.ui.organization-team-switch-test 'frontend-tests.ui.routes-test diff --git a/frontend/test/frontend_tests/ui/measurements_test.cljs b/frontend/test/frontend_tests/ui/measurements_test.cljs new file mode 100644 index 0000000000..725e6d2d24 --- /dev/null +++ b/frontend/test/frontend_tests/ui/measurements_test.cljs @@ -0,0 +1,143 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.ui.measurements-test + (:require + [app.common.geom.rect :as grc] + [app.common.math :as mth] + [app.common.types.shape :as cts] + [app.main.ui.measurements :as msr] + [cljs.test :as t :include-macros true])) + +;; At zoom 1 the badge is 16 high, has 6 of horizontal padding, 6.5 per +;; character, and sits 8 away from the shape: its center is 16 away. +;; A flat path selrect has a minimum side of 0.01, which the measures +;; inputs show too. + +(def ^:private big-vbox {:x -1000 :y -1000 :width 5000 :height 5000}) + +(defn- rect + [x y width height] + (cts/setup-shape {:type :rect :x x :y y :width width :height height})) + +(defn- polyline + "A path through `points`, each a [x y] pair." + [& points] + (let [[[x0 y0] & others] points] + (cts/setup-shape + {:type :path + :content (into [{:command :move-to :params {:x x0 :y y0}}] + (map (fn [[x y]] {:command :line-to :params {:x x :y y}})) + others)}))) + +(defn- close-layout? + [expected layout] + (and (some? layout) + (every? (fn [[k v]] + (let [actual (get layout k)] + (if (number? v) + (mth/close? v actual 0.001) + (= v actual)))) + expected))) + +;; --- Plain shapes + +(t/deftest rect-badge-sits-centered-below-the-shape + (let [layout (msr/size-badge-layout [(rect 0 0 100 50)] 1 big-vbox)] + ;; "100 x 50" has 8 characters: 8 * 6.5 + 2 * 6 + (t/is (close-layout? {:text "100 x 50" :width 64 :height 16 + :cx 50 :cy 66 :rot 0} + layout)))) + +(t/deftest rect-narrower-than-the-badge-hides-it + (t/is (nil? (msr/size-badge-layout [(rect 0 0 40 40)] 1 big-vbox)))) + +(t/deftest rect-lower-than-the-badge-hides-it + (t/is (nil? (msr/size-badge-layout [(rect 0 0 200 10)] 1 big-vbox)))) + +(t/deftest badge-size-scales-with-the-zoom + ;; At zoom 2 the badge is 28.75 x 8, so a 40 x 40 rect fits it. + (let [layout (msr/size-badge-layout [(rect 0 0 40 40)] 2 big-vbox)] + (t/is (close-layout? {:text "40 x 40" :width 28.75 :height 8 :cy 48} + layout)))) + +(t/deftest rect-badge-moves-above-at-the-vbox-bottom + (let [vbox {:x 0 :y 0 :width 1000 :height 60} + layout (msr/size-badge-layout [(rect 0 0 100 50)] 1 vbox)] + (t/is (close-layout? {:cx 50 :cy -16 :rot 0} layout)))) + +(t/deftest multiple-shapes-show-the-bounding-box + (let [layout (msr/size-badge-layout [(rect 0 0 100 50) (rect 200 100 100 50)] + 1 big-vbox)] + (t/is (close-layout? {:text "300 x 150" :cx 150 :cy 166} layout)))) + +;; --- Paths + +(t/deftest path-badge-sits-centered-below-the-path + ;; Three points, so not a straight line: it uses the box badge. + (let [layout (msr/size-badge-layout [(polyline [0 0] [100 100] [200 0])] + 1 big-vbox)] + (t/is (close-layout? {:text "200 x 100" :cx 100 :cy 116 :rot 0} + layout)))) + +(t/deftest path-lower-than-the-badge-hides-it + (t/is (nil? (msr/size-badge-layout [(polyline [0 0] [100 10] [200 0])] + 1 big-vbox))) + ;; A flat path, too: its height is the 0.01 minimum. + (t/is (nil? (msr/size-badge-layout [(polyline [0 100] [100 100] [200 100])] + 1 big-vbox)))) + +(t/deftest path-narrower-than-the-badge-hides-it + (t/is (nil? (msr/size-badge-layout [(polyline [0 0] [10 100] [20 0])] + 1 big-vbox)))) + +(t/deftest path-badge-reads-the-selrect-like-the-inputs + ;; The points enclose a larger box than the selrect, as they do on a + ;; rotated path. The text must match the measures inputs. + (let [path (-> (polyline [0 0] [100 50] [200 0]) + (assoc :points (grc/rect->points (grc/make-rect 0 0 300 300)))) + layout (msr/size-badge-layout [path] 1 big-vbox)] + (t/is (= "200 x 50" (:text layout))))) + +;; --- Straight two-point lines + +(t/deftest horizontal-line-badge-runs-below-the-line + (let [layout (msr/size-badge-layout [(polyline [0 50] [200 50])] 1 big-vbox)] + (t/is (close-layout? {:text "200" :cx 100 :cy 66 :rot 0} layout)))) + +(t/deftest line-drawn-right-to-left-keeps-the-text-upright + (let [layout (msr/size-badge-layout [(polyline [200 50] [0 50])] 1 big-vbox)] + (t/is (close-layout? {:cx 100 :cy 66 :rot 0} layout)))) + +(t/deftest vertical-line-badge-runs-along-the-line + (let [down (msr/size-badge-layout [(polyline [0 0] [0 200])] 1 big-vbox) + up (msr/size-badge-layout [(polyline [0 200] [0 0])] 1 big-vbox)] + (t/is (close-layout? {:text "200" :cx -16 :cy 100 :rot 90} down)) + (t/is (close-layout? {:cx -16 :cy 100 :rot 90} up)))) + +(t/deftest diagonal-line-badge-follows-the-line-angle + (let [layout (msr/size-badge-layout [(polyline [0 0] [100 100])] 1 big-vbox) + offset (/ 16 (mth/sqrt 2))] + (t/is (close-layout? {:text "141.42" :cx (- 50 offset) :cy (+ 50 offset) :rot 45} + layout)))) + +(t/deftest line-shorter-than-the-badge-hides-it + ;; "20" needs a 25 wide badge. + (t/is (nil? (msr/size-badge-layout [(polyline [0 0] [20 0])] 1 big-vbox)))) + +(t/deftest line-badge-shows-the-length-with-two-decimals-at-most + (t/is (= "70.71" (:text (msr/size-badge-layout [(polyline [0 0] [50 50])] 1 big-vbox)))) + (t/is (= "50" (:text (msr/size-badge-layout [(polyline [0 0] [30 40])] 1 big-vbox))))) + +(t/deftest diagonal-line-uses-its-length-to-fit-the-badge + ;; Both sides (33) are narrower than the badge for "46.67" (44.5), but + ;; the line is about 46.67 long, so the badge fits along it. + (t/is (some? (msr/size-badge-layout [(polyline [0 0] [33 33])] 1 big-vbox)))) + +(t/deftest line-badge-moves-to-the-other-side-at-the-vbox-bottom + (let [vbox {:x 0 :y 0 :width 1000 :height 60} + layout (msr/size-badge-layout [(polyline [0 50] [200 50])] 1 vbox)] + (t/is (close-layout? {:cx 100 :cy 34 :rot 0} layout)))) From b2d688a04c73e0c101cc78ed721e8feaf9f1442f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 7 Oct 2026 07:57:10 +0000 Subject: [PATCH 040/145] :lipstick: Stringify ids in comp-processors log props Wrap every id prop in the comp-processors warn logs with str so the log shows plain text instead of the #uuid reader form. Also use :hint instead of :msg and lowercase text, as the rest of the system does. No logic changes, logging only. AI-assisted-by: muse-spark-1.3 --- .../src/app/common/files/comp_processors.cljc | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/common/src/app/common/files/comp_processors.cljc b/common/src/app/common/files/comp_processors.cljc index b74fd0c4b0..993a04c42d 100644 --- a/common/src/app/common/files/comp_processors.cljc +++ b/common/src/app/common/files/comp_processors.cljc @@ -23,18 +23,18 @@ (if (:deleted component) (if (nil? (:objects component)) (do - (log/warn :msg "Adding empty :objects to deleted component" - :component-id (:id component) + (log/warn :hint "adding empty :objects to deleted component" + :component-id (str (:id component)) :component-name (:name component) - :file-id (:id file-data)) + :file-id (str (:id file-data))) (assoc component :objects {})) component) (if (contains? component :objects) (do - (log/warn :msg "Removing :objects from non-deleted component" - :component-id (:id component) + (log/warn :hint "removing :objects from non-deleted component" + :component-id (str (:id component)) :component-name (:name component) - :file-id (:id file-data)) + :file-id (str (:id file-data))) (dissoc component :objects)) component))))) @@ -50,15 +50,15 @@ (fn [shape] (if (false? (:component-root shape)) (do - (log/warn :msg "Normalizing :component-root false on shape" - :shape-id (:id shape) + (log/warn :hint "normalizing :component-root false on shape" + :shape-id (str (:id shape)) :shape-name (:name shape) - :file-id (:id file-data)) + :file-id (str (:id file-data))) {:result :update :updated-shape (dissoc shape :component-root)}) {:result :keep}))) (catch #?(:clj Throwable :cljs :default) e - (log/warn :msg "Failed to normalize :component-root on shapes" - :file-id (:id file-data) + (log/warn :hint "failed to normalize :component-root on shapes" + :file-id (str (:id file-data)) :cause e) file-data))) @@ -82,19 +82,19 @@ (not= (:shape-ref shape) (:id near-match)) (nil? (ctk/get-swap-slot shape))) (let [updated-shape (ctk/set-swap-slot shape (:id near-match))] - (log/warn :msg "Adding missing swap slot to shape" - :shape-id (:id shape) + (log/warn :hint "adding missing swap slot to shape" + :shape-id (str (:id shape)) :shape-name (:name shape) - :swap-slot (:id near-match) - :file-id (:id file) - :container-id (:id container) + :swap-slot (str (:id near-match)) + :file-id (str (:id file)) + :container-id (str (:id container)) :container-type (:type container)) {:result :update :updated-shape updated-shape}) {:result :keep})) {:result :keep}))) (catch #?(:clj Throwable :cljs :default) e - (log/warn :msg "Failed to fix missing swap slots on shapes" - :file-id (:id file-data) + (log/warn :hint "failed to fix missing swap slots on shapes" + :file-id (str (:id file-data)) :cause e) file-data))) @@ -127,21 +127,21 @@ (nil? (:component-file ref-shape)) (dissoc :component-file))] - (log/warn :msg "Syncing component id and file with ref shape" - :shape-id (:id shape) + (log/warn :hint "syncing component id and file with ref shape" + :shape-id (str (:id shape)) :shape-name (:name shape) - :component-id (:component-id shape') - :component-file (:component-file shape') - :ref-shape-id (:id ref-shape) - :file-id (:id file) - :container-id (:id container) + :component-id (str (:component-id shape')) + :component-file (str (:component-file shape')) + :ref-shape-id (str (:id ref-shape)) + :file-id (str (:id file)) + :container-id (str (:id container)) :container-type (:type container)) {:result :update :updated-shape shape'}) {:result :keep})) {:result :keep}))) (catch #?(:clj Throwable :cljs :default) e - (log/warn :msg "Failed to sync component id and file with ref shape" - :file-id (:id file-data) + (log/warn :hint "failed to sync component id and file with ref shape" + :file-id (str (:id file-data)) :cause e) file-data)))] ;; If a copy inside a main is updated, we need to repeat the process for the change to be From 806a9ee98e14d78fcb6a351c9e3b6006d6e3c4b7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 7 Oct 2026 10:41:29 +0200 Subject: [PATCH 041/145] :arrow_up: Update dependencies (#12150) --- backend/deps.edn | 12 +- docs/package.json | 2 +- docs/pnpm-lock.yaml | 98 +- exporter/pnpm-lock.yaml | 139 +- frontend/package.json | 40 +- frontend/packages/ui/package.json | 14 +- frontend/pnpm-lock.yaml | 4131 +++++------------ frontend/pnpm-workspace.yaml | 4 + library/package.json | 2 +- library/pnpm-lock.yaml | 61 +- library/pnpm-workspace.yaml | 2 + mcp/packages/common/package.json | 2 +- mcp/packages/plugin/package.json | 4 +- mcp/packages/server/package.json | 16 +- mcp/pnpm-lock.yaml | 678 +-- mcp/pnpm-workspace.yaml | 1 + .../apps/composable-test-suite/package.json | 8 +- .../composable-test-suite/src/vite-env.d.ts | 1 + .../apps/plugin-api-test-suite/package.json | 2 +- plugins/package.json | 66 +- plugins/pnpm-lock.yaml | 2505 +++++----- 21 files changed, 2928 insertions(+), 4860 deletions(-) create mode 100644 plugins/apps/composable-test-suite/src/vite-env.d.ts diff --git a/backend/deps.edn b/backend/deps.edn index fd5fc81398..4a57cc1abe 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -6,7 +6,7 @@ org.clojure/clojure {:mvn/version "1.12.6"} org.clojure/tools.namespace {:mvn/version "1.5.1"} - com.github.luben/zstd-jni {:mvn/version "1.5.7-20"} + com.github.luben/zstd-jni {:mvn/version "1.5.7-21"} io.prometheus/simpleclient {:mvn/version "0.16.0"} io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} @@ -37,7 +37,7 @@ {:mvn/version "1.3.1118"} metosin/reitit-core {:mvn/version "0.11.0"} - nrepl/nrepl {:mvn/version "1.7.0"} + nrepl/nrepl {:mvn/version "1.8.0"} org.postgresql/postgresql {:mvn/version "42.7.13"} org.xerial/sqlite-jdbc {:mvn/version "3.53.4.0"} @@ -50,7 +50,7 @@ buddy/buddy-sign {:mvn/version "3.6.1-359"} org.passay/passay {:mvn/version "2.0.0"} - com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"} + com.github.ben-manes.caffeine/caffeine {:mvn/version "3.3.0"} org.jsoup/jsoup {:mvn/version "1.23.2"} @@ -64,10 +64,10 @@ ;; Pretty Print specs pretty-spec/pretty-spec {:mvn/version "0.1.4"} - software.amazon.awssdk/s3 {:mvn/version "2.55.6"} - software.amazon.awssdk/sts {:mvn/version "2.55.6"} + software.amazon.awssdk/s3 {:mvn/version "2.55.11"} + software.amazon.awssdk/sts {:mvn/version "2.55.11"} - com.ladybugdb/lbug {:mvn/version "0.20.4"} + com.ladybugdb/lbug {:mvn/version "0.21.2"} ;; Required by Arrow RootAllocator (lbug only pulls arrow-memory-core). org.apache.arrow/arrow-memory-netty {:mvn/version "19.0.0"}} diff --git a/docs/package.json b/docs/package.json index fb43d456ef..dd15b23ccb 100644 --- a/docs/package.json +++ b/docs/package.json @@ -26,7 +26,7 @@ "dependencies": { "@11ty/eleventy": "^3.1.6", "@11ty/eleventy-navigation": "^1.0.5", - "@11ty/eleventy-plugin-rss": "^3.0.0", + "@11ty/eleventy-plugin-rss": "^3.1.0", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2", "@tigersway/eleventy-plugin-ancestry": "^1.0.3", "@types/markdown-it": "14.2.0", diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 191f3ace31..ed94c030a5 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -173,8 +173,8 @@ importers: specifier: ^1.0.5 version: 1.0.5 '@11ty/eleventy-plugin-rss': - specifier: ^3.0.0 - version: 3.0.0 + specifier: ^3.1.0 + version: 3.1.0 '@11ty/eleventy-plugin-syntaxhighlight': specifier: ^5.0.2 version: 5.0.2 @@ -233,8 +233,8 @@ packages: resolution: {integrity: sha512-QK1tRFBhQdZASnYU8GMzpTdsMMFLVAkuU0gVVILqNyp09xJJZb81kAS3AFrNrwBCsgLxTdWHJ8N64+OTTsoKkA==} engines: {node: '>=18'} - '@11ty/eleventy-plugin-rss@3.0.0': - resolution: {integrity: sha512-kKW4DcR57xAyRx0e8gNhKh56ahHVEaAj8/TuXQDnw+B46ig2bWADJAlyj/GdV37IG5ja9dZ4SgKZrs/CHz6YWQ==} + '@11ty/eleventy-plugin-rss@3.1.0': + resolution: {integrity: sha512-eby7wIYU3XBTAel8qPzbZSEhzNg/WXcAqNULYRqzMBU+gVlx4SL2mUvWDYxkp72KJqefnMMIo2Zz6w09N6S5XQ==} '@11ty/eleventy-plugin-syntaxhighlight@5.0.2': resolution: {integrity: sha512-T6xVVRDJuHlrFMHbUiZkHjj5o1IlLzZW+1IL9eUsyXFU7rY2ztcYhZew/64vmceFFpQwzuSfxQOXxTJYmKkQ+A==} @@ -293,8 +293,8 @@ packages: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} - acorn@8.18.0: - resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} engines: {node: '>=0.4.0'} hasBin: true @@ -308,8 +308,8 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - argparse@3.0.1: - resolution: {integrity: sha512-nM4mHF/KM1v59ZNKX7zfusQz5wUAxR511YG8Vo6TyiV4aqhu++rbJW4v04xsWhpSsHFj66flT8P7znVpyO20xQ==} + argparse@3.0.2: + resolution: {integrity: sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==} asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -333,8 +333,8 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.18: - resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -462,8 +462,8 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} - entities@8.0.0: - resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} engines: {node: '>=20.19.0'} errno@1.0.0: @@ -601,12 +601,12 @@ packages: resolution: {integrity: sha512-ZFar/L4ngX7wZh2QX+Fiftmuf0igWJsrJtfizrovWifF1gAWkfmRa5Z1m0LQZbm0hKCHRDYhLRSLFrSqNe4EJA==} engines: {node: '>=6.0'} - js-yaml@3.15.1: - resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} + js-yaml@3.15.2: + resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} hasBin: true - js-yaml@4.3.1: - resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true junk@3.1.0: @@ -627,8 +627,8 @@ packages: linkify-it@6.1.0: resolution: {integrity: sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==} - liquidjs@10.29.0: - resolution: {integrity: sha512-pCVOhs6FLAR8su3ItJ07diN26t6W5dHQRnmTMy8HPyTFuv1+oSCVJIGp5pGjfQyOZfh50KswvKtMTp6p4JEIdw==} + liquidjs@10.30.0: + resolution: {integrity: sha512-Fw4wA+8CZsJkaeLEIlPE3AoponfjEZhj7FQvYNcAEcg7jNRik7mEMoaco382FBdrYlykVf6w4vClPNctz2OYjg==} engines: {node: '>=16'} hasBin: true @@ -720,6 +720,10 @@ packages: chokidar: optional: true + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -867,8 +871,8 @@ packages: uc.micro@3.0.0: resolution: {integrity: sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==} - undici@7.29.0: - resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + undici@7.30.0: + resolution: {integrity: sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==} engines: {node: '>=20.18.1'} unpipe@1.0.0: @@ -887,8 +891,8 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} - ws@8.21.3: - resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -904,7 +908,7 @@ snapshots: '@11ty/dependency-tree-esm@2.0.4': dependencies: '@11ty/eleventy-utils': 2.0.7 - acorn: 8.18.0 + acorn: 8.19.0 dependency-graph: 1.0.0 normalize-path: 3.0.0 @@ -925,7 +929,7 @@ snapshots: send: 1.2.1 ssri: 11.0.0 urlpattern-polyfill: 10.1.0 - ws: 8.21.3 + ws: 8.22.0 transitivePeerDependencies: - bufferutil - supports-color @@ -954,14 +958,12 @@ snapshots: - posthtml - supports-color - '@11ty/eleventy-plugin-rss@3.0.0': + '@11ty/eleventy-plugin-rss@3.1.0': dependencies: '@11ty/eleventy-utils': 2.0.7 '@11ty/posthtml-urls': 1.0.3 - debug: 4.4.3 + obug: 3.0.0 posthtml: 0.16.7 - transitivePeerDependencies: - - supports-color '@11ty/eleventy-plugin-syntaxhighlight@5.0.2': dependencies: @@ -988,9 +990,9 @@ snapshots: filesize: 10.1.6 gray-matter: 4.0.3 iso-639-1: 3.1.6 - js-yaml: 4.3.1 + js-yaml: 4.3.2 kleur: 4.1.5 - liquidjs: 10.29.0 + liquidjs: 10.30.0 luxon: 3.7.2 markdown-it: 14.3.2 minimist: 1.2.8 @@ -1053,9 +1055,9 @@ snapshots: acorn-walk@8.3.5: dependencies: - acorn: 8.18.0 + acorn: 8.19.0 - acorn@8.18.0: {} + acorn@8.19.0: {} anymatch@3.1.3: dependencies: @@ -1068,7 +1070,7 @@ snapshots: argparse@2.0.1: {} - argparse@3.0.1: {} + argparse@3.0.2: {} asap@2.0.6: {} @@ -1091,7 +1093,7 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@1.1.18: + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 @@ -1120,7 +1122,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.29.0 + undici: 7.30.0 whatwg-mimetype: 4.0.0 chokidar@3.6.0: @@ -1242,7 +1244,7 @@ snapshots: entities@7.0.1: {} - entities@8.0.0: {} + entities@8.1.0: {} errno@1.0.0: dependencies: @@ -1254,7 +1256,7 @@ snapshots: esm-import-transformer@3.0.5: dependencies: - acorn: 8.18.0 + acorn: 8.19.0 esprima@4.0.1: {} @@ -1305,7 +1307,7 @@ snapshots: gray-matter@4.0.3: dependencies: - js-yaml: 3.15.1 + js-yaml: 3.15.2 kind-of: 6.0.3 section-matter: 1.0.0 strip-bom-string: 1.0.0 @@ -1367,12 +1369,12 @@ snapshots: iso-639-1@3.1.6: {} - js-yaml@3.15.1: + js-yaml@3.15.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.3.1: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -1390,7 +1392,7 @@ snapshots: dependencies: uc.micro: 3.0.0 - liquidjs@10.29.0: + liquidjs@10.30.0: dependencies: commander: 10.0.1 @@ -1418,8 +1420,8 @@ snapshots: markdown-it@15.0.2: dependencies: - argparse: 3.0.1 - entities: 8.0.0 + argparse: 3.0.2 + entities: 8.1.0 linkify-it: 6.1.0 mdurl: 2.1.0 punycode.js: 2.3.1 @@ -1441,7 +1443,7 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 1.1.18 + brace-expansion: 1.1.21 minimist@1.2.8: {} @@ -1457,7 +1459,7 @@ snapshots: node-retrieve-globals@6.0.1: dependencies: - acorn: 8.18.0 + acorn: 8.19.0 acorn-walk: 8.3.5 esm-import-transformer: 3.0.5 @@ -1475,6 +1477,8 @@ snapshots: optionalDependencies: chokidar: 3.6.0 + obug@3.0.0: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -1604,7 +1608,7 @@ snapshots: uc.micro@3.0.0: {} - undici@7.29.0: {} + undici@7.30.0: {} unpipe@1.0.0: {} @@ -1616,4 +1620,4 @@ snapshots: whatwg-mimetype@4.0.0: {} - ws@8.21.3: {} + ws@8.22.0: {} diff --git a/exporter/pnpm-lock.yaml b/exporter/pnpm-lock.yaml index 36db736d8d..c15da53ffa 100644 --- a/exporter/pnpm-lock.yaml +++ b/exporter/pnpm-lock.yaml @@ -240,8 +240,8 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - b4a@1.8.1: - resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + b4a@1.9.0: + resolution: {integrity: sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==} peerDependencies: react-native-b4a: '*' peerDependenciesMeta: @@ -252,32 +252,28 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - bare-events@2.9.1: - resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + bare-events@2.9.2: + resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} peerDependencies: bare-abort-controller: '*' peerDependenciesMeta: bare-abort-controller: optional: true - bare-fs@4.7.2: - resolution: {integrity: sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==} - engines: {bare: '>=1.16.0'} + bare-fs@4.8.2: + resolution: {integrity: sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==} + engines: {bare: '>=1.28.0'} peerDependencies: bare-buffer: '*' peerDependenciesMeta: bare-buffer: optional: true - bare-os@3.9.1: - resolution: {integrity: sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==} - engines: {bare: '>=1.14.0'} + bare-path@3.1.2: + resolution: {integrity: sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==} - bare-path@3.0.1: - resolution: {integrity: sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==} - - bare-stream@2.13.1: - resolution: {integrity: sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==} + bare-stream@2.13.4: + resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} peerDependencies: bare-abort-controller: '*' bare-buffer: '*' @@ -290,8 +286,8 @@ packages: bare-events: optional: true - bare-url@2.4.5: - resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} + bare-url@2.5.4: + resolution: {integrity: sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==} base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -300,8 +296,8 @@ packages: resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==} engines: {node: '>=20.19.0'} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} buffer-crc32@1.0.0: @@ -330,8 +326,8 @@ packages: resolution: {integrity: sha512-8BIbcC6tPZMv2/PD4PrB3CtKks7LmAWhA7FRZK/6/b/WJ2RUz00KCga7gq50S4/RhZ22kxefRFGgsuts0v3eoA==} engines: {node: '>= 0.8'} - core-js-pure@3.49.0: - resolution: {integrity: sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==} + core-js-pure@3.50.0: + resolution: {integrity: sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -401,8 +397,8 @@ packages: resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==} engines: {node: '>=20.19.0'} - entities@8.0.0: - resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} engines: {node: '>=20.19.0'} event-target-shim@5.0.1: @@ -470,8 +466,8 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} ms@2.1.3: @@ -527,15 +523,15 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - sax@1.6.0: - resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} engines: {node: '>=11.0.0'} setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} source-map-support@0.5.21: @@ -552,8 +548,8 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - streamx@2.27.0: - resolution: {integrity: sha512-WZ189TKnHoAokYHvwzaAQMpd55cgUmFIcJFzBSgGcb886jau5DL+XdDhTWV4ps3FLvk+OORp0dLRTPsLZ21CSA==} + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -561,8 +557,8 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - tar-stream@3.2.0: - resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + tar-stream@3.2.2: + resolution: {integrity: sha512-+8NeqHRjQWH9nYlwo2gamAMImZCVzI4UoEgDpWorBt9OEfppiZn+uSkskzQKPWIZyji/C8fpWO7u69G0DX0tbg==} teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} @@ -612,7 +608,7 @@ snapshots: '@babel/runtime-corejs3@7.29.7': dependencies: - core-js-pure: 3.49.0 + core-js-pure: 3.50.0 '@ioredis/commands@2.0.0': {} @@ -622,7 +618,7 @@ snapshots: css-tree: 3.2.1 csso: 5.0.5 lodash: 4.18.1 - sax: 1.6.0 + sax: 1.6.1 abort-controller@3.0.0: dependencies: @@ -637,7 +633,7 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.7.0 readdir-glob: 3.0.0 - tar-stream: 3.2.0 + tar-stream: 3.2.2 zip-stream: 7.0.5 transitivePeerDependencies: - bare-abort-controller @@ -646,47 +642,44 @@ snapshots: async@3.2.6: {} - b4a@1.8.1: {} + b4a@1.9.0: {} balanced-match@4.0.4: {} - bare-events@2.9.1: {} + bare-events@2.9.2: {} - bare-fs@4.7.2: + bare-fs@4.8.2: dependencies: - bare-events: 2.9.1 - bare-path: 3.0.1 - bare-stream: 2.13.1(bare-events@2.9.1) - bare-url: 2.4.5 + bare-events: 2.9.2 + bare-path: 3.1.2 + bare-stream: 2.13.4(bare-events@2.9.2) + bare-url: 2.5.4 fast-fifo: 1.3.2 transitivePeerDependencies: - bare-abort-controller - react-native-b4a - bare-os@3.9.1: {} + bare-path@3.1.2: {} - bare-path@3.0.1: + bare-stream@2.13.4(bare-events@2.9.2): dependencies: - bare-os: 3.9.1 - - bare-stream@2.13.1(bare-events@2.9.1): - dependencies: - streamx: 2.27.0 + b4a: 1.9.0 + streamx: 2.28.1 teex: 1.0.1 optionalDependencies: - bare-events: 2.9.1 + bare-events: 2.9.2 transitivePeerDependencies: - react-native-b4a - bare-url@2.4.5: + bare-url@2.5.4: dependencies: - bare-path: 3.0.1 + bare-path: 3.1.2 base64-js@1.5.1: {} boolbase@2.0.0: {} - brace-expansion@5.0.9: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -716,7 +709,7 @@ snapshots: depd: 2.0.0 keygrip: 1.1.0 - core-js-pure@3.49.0: {} + core-js-pure@3.50.0: {} core-util-is@1.0.3: {} @@ -738,12 +731,12 @@ snapshots: css-tree@2.2.1: dependencies: mdn-data: 2.0.28 - source-map-js: 1.2.1 + source-map-js: 1.2.2 css-tree@3.2.1: dependencies: mdn-data: 2.27.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 css-what@8.0.0: {} @@ -765,7 +758,7 @@ snapshots: dependencies: domelementtype: 3.0.0 domhandler: 6.0.1 - entities: 8.0.0 + entities: 8.1.0 domelementtype@3.0.0: {} @@ -779,13 +772,13 @@ snapshots: domelementtype: 3.0.0 domhandler: 6.0.1 - entities@8.0.0: {} + entities@8.1.0: {} event-target-shim@5.0.1: {} events-universal@1.0.1: dependencies: - bare-events: 2.9.1 + bare-events: 2.9.2 transitivePeerDependencies: - bare-abort-controller @@ -841,9 +834,9 @@ snapshots: mdn-data@2.27.1: {} - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 ms@2.1.3: {} @@ -890,7 +883,7 @@ snapshots: readdir-glob@3.0.0: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.6 redis-errors@1.2.0: {} @@ -898,11 +891,11 @@ snapshots: safe-buffer@5.2.1: {} - sax@1.6.0: {} + sax@1.6.1: {} setprototypeof@1.2.0: {} - source-map-js@1.2.1: {} + source-map-js@1.2.2: {} source-map-support@0.5.21: dependencies: @@ -915,7 +908,7 @@ snapshots: statuses@2.0.2: {} - streamx@2.27.0: + streamx@2.28.1: dependencies: events-universal: 1.0.1 fast-fifo: 1.3.2 @@ -932,12 +925,12 @@ snapshots: dependencies: safe-buffer: 5.2.1 - tar-stream@3.2.0: + tar-stream@3.2.2: dependencies: - b4a: 1.8.1 - bare-fs: 4.7.2 + b4a: 1.9.0 + bare-fs: 4.8.2 fast-fifo: 1.3.2 - streamx: 2.27.0 + streamx: 2.28.1 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -945,14 +938,14 @@ snapshots: teex@1.0.1: dependencies: - streamx: 2.27.0 + streamx: 2.28.1 transitivePeerDependencies: - bare-abort-controller - react-native-b4a text-decoder@1.2.7: dependencies: - b4a: 1.8.1 + b4a: 1.9.0 transitivePeerDependencies: - react-native-b4a @@ -968,7 +961,7 @@ snapshots: xml-js@1.6.11: dependencies: - sax: 1.6.0 + sax: 1.6.1 xregexp@5.1.3: dependencies: diff --git a/frontend/package.json b/frontend/package.json index 7618a35d4c..432db7a070 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,15 +59,15 @@ "@penpot/tokenscript": "link:packages/tokenscript", "@penpot/ua-parser": "penpot/ua-parser#1.0.0", "@playwright/test": "1.63.0", - "@storybook/addon-docs": "10.6.0", - "@storybook/addon-themes": "10.6.0", - "@storybook/addon-vitest": "10.6.0", - "@storybook/react-vite": "10.6.0", + "@storybook/addon-docs": "10.6.1", + "@storybook/addon-themes": "10.6.1", + "@storybook/addon-vitest": "10.6.1", + "@storybook/react-vite": "10.6.1", "@tokens-studio/sd-transforms": "2.0.3", - "@types/node": "^26.6.3", - "@vitest/browser": "5.0.2", - "@vitest/browser-playwright": "5.0.2", - "@vitest/coverage-v8": "5.0.2", + "@types/node": "^26.6.4", + "@vitest/browser": "5.0.3", + "@vitest/browser-playwright": "5.0.3", + "@vitest/coverage-v8": "5.0.3", "@zip.js/zip.js": "2.8.34", "autoprefixer": "^10.6.1", "compression": "^1.8.2", @@ -75,17 +75,17 @@ "date-fns": "^4.4.0", "esbuild": "^0.28.2", "eventsource-parser": "^4.1.1", - "express": "^5.1.0", + "express": "^5.2.1", "fancy-log": "^2.0.0", "getopts": "^2.3.0", "gettext-parser": "^9.1.1", "highlight.js": "^11.12.0", "js-beautify": "^2.0.3", - "jsdom": "^30.1.1", + "jsdom": "^30.1.2", "lodash": "^4.18.1", "lodash.debounce": "^4.0.8", "map-stream": "0.0.7", - "marked": "^18.0.14", + "marked": "^18.1.0", "mkdirp": "^3.0.1", "mustache": "^4.2.0", "nodemon": "^3.1.14", @@ -93,7 +93,7 @@ "opentype.js": "^2.0.0", "p-limit": "^7.3.3", "playwright": "1.63.0", - "postcss": "^8.5.28", + "postcss": "^8.5.29", "postcss-clean": "^1.2.2", "postcss-modules": "^9.0.1", "postcss-scss": "^4.0.9", @@ -107,24 +107,24 @@ "react-virtualized": "^9.22.6", "rimraf": "^6.1.3", "rxjs": "8.0.0-alpha.14", - "sass": "^1.105.0", - "sass-embedded": "^1.105.0", + "sass": "^1.105.1", + "sass-embedded": "^1.105.1", "sax": "^1.6.1", "scheduler": "^0.28.0", "source-map-support": "^0.5.21", - "storybook": "10.6.0", + "storybook": "10.6.1", "style-dictionary": "5.5.5", - "stylelint": "^17.15.0", + "stylelint": "^17.16.0", "stylelint-config-standard-scss": "^17.0.0", "stylelint-plugin-logical-css": "^2.1.0", "stylelint-scss": "^7.3.0", "svg-sprite": "^2.0.4", "tdigest": "^0.1.3", "tinycolor2": "^1.6.0", - "typescript": "^6.0.2", - "vite": "^8.3.1", - "vitest": "^5.0.2", - "wait-on": "^9.1.0", + "typescript": "^6.0.3", + "vite": "^8.3.3", + "vitest": "^5.0.3", + "wait-on": "^9.5.1", "watcher": "^2.3.1", "workerpool": "^10.0.3", "xregexp": "^5.1.3" diff --git a/frontend/packages/ui/package.json b/frontend/packages/ui/package.json index d5b9e71100..1b8f97c154 100644 --- a/frontend/packages/ui/package.json +++ b/frontend/packages/ui/package.json @@ -25,21 +25,17 @@ "devDependencies": { "@babel/core": "^8.0.6", "@babel/preset-react": "^8.0.1", - "@storybook/react": "10.6.0", - "@storybook/react-vite": "10.6.0", + "@storybook/react": "10.6.1", + "@storybook/react-vite": "10.6.1", "@testing-library/dom": "10.4.2", "@testing-library/react": "16.3.3", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", - "@vitejs/plugin-react": "^6.1.1", + "@vitejs/plugin-react": "^6.1.2", "babel-plugin-react-compiler": "^1.0.0", - "eslint-plugin-import": "2.32.0", - "eslint-plugin-jsx-a11y": "6.10.2", - "eslint-plugin-react": "7.37.5", - "eslint-plugin-react-hooks": "7.1.1", "react-compiler-runtime": "^1.0.0", - "storybook": "10.6.0", - "vite-plugin-dts": "^5.1.1" + "storybook": "10.6.1", + "vite-plugin-dts": "^5.1.2" }, "dependencies": { "react-aria-components": "^1.20.0" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5bc040bee7..35c7052b59 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -176,6 +176,10 @@ overrides: postcss@<8.5.10: ^8.5.10 yaml@>=2.0.0 <2.8.3: ^2.8.3 playwright@>=1.61.1 <2.0.0-0: 1.62.1 + postcss@<=8.5.11: ^8.5.12 + postcss@<=8.5.17: ^8.5.18 + postcss@<=8.5.22: ^8.5.23 + shell-quote@>=1.8.4 <1.11.0: ^1.11.0 patchedDependencies: '@zip.js/zip.js@2.8.34': 7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95 @@ -216,38 +220,38 @@ importers: specifier: 1.63.0 version: 1.63.0 '@storybook/addon-docs': - specifier: 10.6.0 - version: 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0)) + specifier: 10.6.1 + version: 10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0)) '@storybook/addon-themes': - specifier: 10.6.0 - version: 10.6.0(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0)) + specifier: 10.6.1 + version: 10.6.1(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0)) '@storybook/addon-vitest': - specifier: 10.6.0 - version: 10.6.0(@vitest/browser-playwright@5.0.2)(@vitest/browser@5.0.2)(@vitest/runner@4.1.10)(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(vitest@5.0.2) + specifier: 10.6.1 + version: 10.6.1(@vitest/browser-playwright@5.0.3)(@vitest/browser@5.0.3)(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(vitest@5.0.3) '@storybook/react-vite': - specifier: 10.6.0 - version: 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rollup@4.61.1)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@5.5.0)(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + specifier: 10.6.1 + version: 10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@5.5.0)(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) '@tokens-studio/sd-transforms': specifier: 2.0.3 - version: 2.0.3(style-dictionary@5.5.5(tslib@2.8.1)) + version: 2.0.3(style-dictionary@5.5.5) '@types/node': - specifier: ^26.6.3 - version: 26.6.3 + specifier: ^26.6.4 + version: 26.6.4 '@vitest/browser': - specifier: 5.0.2 - version: 5.0.2(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))(vitest@5.0.2) + specifier: 5.0.3 + version: 5.0.3(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))(vitest@5.0.3) '@vitest/browser-playwright': - specifier: 5.0.2 - version: 5.0.2(playwright@1.62.1)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))(vitest@5.0.2) + specifier: 5.0.3 + version: 5.0.3(playwright@1.62.1)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))(vitest@5.0.3) '@vitest/coverage-v8': - specifier: 5.0.2 - version: 5.0.2(@vitest/browser@5.0.2)(vitest@5.0.2) + specifier: 5.0.3 + version: 5.0.3(@vitest/browser@5.0.3)(vitest@5.0.3) '@zip.js/zip.js': specifier: 2.8.34 version: 2.8.34(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95) autoprefixer: specifier: ^10.6.1 - version: 10.6.1(postcss@8.5.28) + version: 10.6.1(postcss@8.5.29) compression: specifier: ^1.8.2 version: 1.8.2(supports-color@5.5.0) @@ -264,7 +268,7 @@ importers: specifier: ^4.1.1 version: 4.1.1 express: - specifier: ^5.1.0 + specifier: ^5.2.1 version: 5.2.1(supports-color@5.5.0) fancy-log: specifier: ^2.0.0 @@ -282,8 +286,8 @@ importers: specifier: ^2.0.3 version: 2.0.3 jsdom: - specifier: ^30.1.1 - version: 30.1.1(canvas@3.2.3) + specifier: ^30.1.2 + version: 30.1.2(canvas@3.2.3) lodash: specifier: ^4.18.1 version: 4.18.1 @@ -294,8 +298,8 @@ importers: specifier: 0.0.7 version: 0.0.7 marked: - specifier: ^18.0.14 - version: 18.0.14 + specifier: ^18.1.0 + version: 18.1.0 mkdirp: specifier: ^3.0.1 version: 3.0.1 @@ -318,17 +322,17 @@ importers: specifier: 1.62.1 version: 1.62.1 postcss: - specifier: ^8.5.28 - version: 8.5.28 + specifier: ^8.5.29 + version: 8.5.29 postcss-clean: specifier: ^1.2.2 version: 1.2.2 postcss-modules: specifier: ^9.0.1 - version: 9.0.1(postcss@8.5.28) + version: 9.0.1(postcss@8.5.29) postcss-scss: specifier: ^4.0.9 - version: 4.0.9(postcss@8.5.28) + version: 4.0.9(postcss@8.5.29) prettier: specifier: 3.9.9 version: 3.9.9 @@ -360,11 +364,11 @@ importers: specifier: 8.0.0-alpha.14 version: 8.0.0-alpha.14 sass: - specifier: ^1.105.0 - version: 1.105.0 + specifier: ^1.105.1 + version: 1.105.1 sass-embedded: - specifier: ^1.105.0 - version: 1.105.0 + specifier: ^1.105.1 + version: 1.105.1 sax: specifier: ^1.6.1 version: 1.6.1 @@ -375,23 +379,23 @@ importers: specifier: ^0.5.21 version: 0.5.21 storybook: - specifier: 10.6.0 - version: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + specifier: 10.6.1 + version: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) style-dictionary: specifier: 5.5.5 - version: 5.5.5(tslib@2.8.1) + version: 5.5.5 stylelint: - specifier: ^17.15.0 - version: 17.15.0(supports-color@5.5.0)(typescript@6.0.3) + specifier: ^17.16.0 + version: 17.16.0(supports-color@5.5.0)(typescript@6.0.3) stylelint-config-standard-scss: specifier: ^17.0.0 - version: 17.0.0(postcss@8.5.28)(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)) + version: 17.0.0(postcss@8.5.29)(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)) stylelint-plugin-logical-css: specifier: ^2.1.0 - version: 2.1.0(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)) + version: 2.1.0(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)) stylelint-scss: specifier: ^7.3.0 - version: 7.3.0(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)) + version: 7.3.0(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)) svg-sprite: specifier: ^2.0.4 version: 2.0.4 @@ -402,17 +406,17 @@ importers: specifier: ^1.6.0 version: 1.6.0 typescript: - specifier: ^6.0.2 + specifier: ^6.0.3 version: 6.0.3 vite: - specifier: ^8.3.1 - version: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) + specifier: ^8.3.3 + version: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) vitest: - specifier: ^5.0.2 - version: 5.0.2(@types/node@26.6.3)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1(canvas@3.2.3))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + specifier: ^5.0.3 + version: 5.0.3(@types/node@26.6.4)(@vitest/browser-playwright@5.0.3)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(jsdom@30.1.2(canvas@3.2.3))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) wait-on: - specifier: ^9.1.0 - version: 9.1.0(debug@4.4.3(supports-color@5.5.0))(supports-color@5.5.0) + specifier: ^9.5.1 + version: 9.5.1(debug@4.4.3(supports-color@5.5.0))(supports-color@5.5.0) watcher: specifier: ^2.3.1 version: 2.3.1 @@ -466,11 +470,11 @@ importers: specifier: ^8.0.1 version: 8.0.1(@babel/core@8.0.6) '@storybook/react': - specifier: 10.6.0 - version: 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@10.2.2)(typescript@6.0.3) + specifier: 10.6.1 + version: 10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@10.2.2)(typescript@6.0.3) '@storybook/react-vite': - specifier: 10.6.0 - version: 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rollup@4.61.1)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + specifier: 10.6.1 + version: 10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) '@testing-library/dom': specifier: 10.4.2 version: 10.4.2 @@ -484,32 +488,20 @@ importers: specifier: ^19.3.0 version: 19.3.0(@types/react@19.3.0) '@vitejs/plugin-react': - specifier: ^6.1.1 - version: 6.1.1(babel-plugin-react-compiler@1.0.0)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + specifier: ^6.1.2 + version: 6.1.2(babel-plugin-react-compiler@1.0.0)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 - eslint-plugin-import: - specifier: 2.32.0 - version: 2.32.0(eslint@9.39.2(supports-color@10.2.2))(supports-color@10.2.2) - eslint-plugin-jsx-a11y: - specifier: 6.10.2 - version: 6.10.2(eslint@9.39.2(supports-color@10.2.2)) - eslint-plugin-react: - specifier: 7.37.5 - version: 7.37.5(eslint@9.39.2(supports-color@10.2.2)) - eslint-plugin-react-hooks: - specifier: 7.1.1 - version: 7.1.1(eslint@9.39.2(supports-color@10.2.2))(supports-color@10.2.2) react-compiler-runtime: specifier: ^1.0.0 version: 1.0.0(react@19.3.0) storybook: - specifier: 10.6.0 - version: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + specifier: 10.6.1 + version: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) vite-plugin-dts: - specifier: ^5.1.1 - version: 5.1.1(@microsoft/api-extractor@7.56.2(@types/node@26.6.3))(esbuild@0.28.2)(rolldown@1.2.1)(rollup@4.61.1)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + specifier: ^5.1.2 + version: 5.1.2(esbuild@0.28.2)(rolldown@1.2.12)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) text-editor: devDependencies: @@ -518,16 +510,16 @@ importers: version: 1.63.0 '@types/node': specifier: ^26.6.3 - version: 26.6.3 + version: 26.6.4 '@vitest/browser': specifier: ^5.0.2 - version: 5.0.2(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))(vitest@5.0.2) + version: 5.0.3(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))(vitest@5.0.3) '@vitest/coverage-v8': specifier: ^5.0.2 - version: 5.0.2(@vitest/browser@5.0.2)(vitest@5.0.2) + version: 5.0.3(@vitest/browser@5.0.3)(vitest@5.0.3) '@vitest/ui': specifier: ^5.0.2 - version: 5.0.2(vitest@5.0.2) + version: 5.0.3(vitest@5.0.3) canvas: specifier: ^3.2.3 version: 3.2.3 @@ -536,7 +528,7 @@ importers: version: 0.28.2 jsdom: specifier: ^30.0.1 - version: 30.1.1(canvas@3.2.3) + version: 30.1.2(canvas@3.2.3) playwright: specifier: 1.62.1 version: 1.62.1 @@ -545,38 +537,34 @@ importers: version: 3.9.9 vite: specifier: ^8.2.0 - version: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) + version: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) vitest: specifier: ^5.0.2 - version: 5.0.2(@types/node@26.6.3)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1(canvas@3.2.3))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + version: 5.0.3(@types/node@26.6.4)(@vitest/browser-playwright@5.0.3)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(jsdom@30.1.2(canvas@3.2.3))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) packages: '@adobe/css-tools@4.5.0': resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} - '@ark/schema@0.56.0': - resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==} + '@ark/schema@0.56.6': + resolution: {integrity: sha512-OmLv80BCF2ib+zjecrXDosip/CSnZSYBGjblhqWYZWFOzdLQka6QymHToE9u7Z/HZW2KA8Hwc8MCNXoI3uGSIA==} - '@ark/util@0.56.0': - resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} + '@ark/util@0.56.6': + resolution: {integrity: sha512-mHqq0f0HyDd9YadpFl25w0QECAP7d8HGIUpEC85duD+L+xr4i/CwfOid/Z7zJFFF/47oLt6+pZB9RbqWpBYTYg==} - '@asamuzakjp/css-color@7.1.2': - resolution: {integrity: sha512-99DHAnXDB5z6EEK+9GMpVI7Mw4oxj97dY5bpOzMnjADQWxI8rN6TvTduuFLUhUMlS7/CfVZ06tcZsus6cltnNw==} + '@asamuzakjp/css-color@7.1.3': + resolution: {integrity: sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - '@asamuzakjp/dom-selector@9.2.2': - resolution: {integrity: sha512-lSWTBMjAcmu2xn5yEDU7jh6QDV+C8GEKtdJ4pIQhXh26RkKQ7S3FuQlt+zZkUbTdJ4d3XyV1kPI/G0zWXxqaqw==} + '@asamuzakjp/dom-selector@9.2.4': + resolution: {integrity: sha512-YnVzxDxLaqL0t7Q4wfcgHZjg55Wm6y5EaO7Bhu3q9J7wq/8iw3PhsnWiOQhYekx4VexnbkgWpTT6n+QIV7kczw==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/code-frame@8.0.0': - resolution: {integrity: sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/code-frame@8.0.6': resolution: {integrity: sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==} engines: {node: ^22.18.0 || >=24.11.0} @@ -597,18 +585,10 @@ packages: resolution: {integrity: sha512-5zwYt1V4ji3mXKKRIAkohSJ29WepxNwxQi+7y7P5AcVPh1dcxnZBU2cfxHnPwS+u9+lZnXPsv30TQaF2xg+Tfg==} engines: {node: ^22.18.0 || >=24.11.0} - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.29.8': resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} - '@babel/generator@8.0.0': - resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/generator@8.0.6': resolution: {integrity: sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==} engines: {node: ^22.18.0 || >=24.11.0} @@ -629,10 +609,6 @@ packages: resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - '@babel/helper-globals@8.0.0': - resolution: {integrity: sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-globals@8.0.6': resolution: {integrity: sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==} engines: {node: ^22.18.0 || >=24.11.0} @@ -661,10 +637,6 @@ packages: resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@8.0.0': - resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-string-parser@8.0.6': resolution: {integrity: sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==} engines: {node: ^22.18.0 || >=24.11.0} @@ -673,10 +645,6 @@ packages: resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@8.0.2': - resolution: {integrity: sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-validator-identifier@8.0.6': resolution: {integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==} engines: {node: ^22.18.0 || >=24.11.0} @@ -697,21 +665,11 @@ packages: resolution: {integrity: sha512-fQtPOXjYOYv85PIdwotp2TJGVYOycX0PQq+l844fFAxOULtBy8BVF35GyeueX0r4KvDthqPH5xAI1clQPk/2uA==} engines: {node: ^22.18.0 || >=24.11.0} - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.29.8': - resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@8.0.0': - resolution: {integrity: sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==} - engines: {node: ^22.18.0 || >=24.11.0} - hasBin: true - '@babel/parser@8.0.6': resolution: {integrity: sha512-LpGDIYJAzc3Y3PT9x+FxBrGYu2PlWxLmyN7SwPTzaX0LSwEmFJBsRnPBqmaXF2r1v+Zhz6lNiEJ+7yKtM5+Ugg==} engines: {node: ^22.18.0 || >=24.11.0} @@ -769,34 +727,18 @@ packages: resolution: {integrity: sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==} engines: {node: ^22.18.0 || >=24.11.0} - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.8': resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} engines: {node: '>=6.9.0'} - '@babel/traverse@8.0.0': - resolution: {integrity: sha512-bxTj/W2VclGE6CctlfQOpxg8MPDzXArRqkOBePw8EHfebcjF7fETWSS3BriEECo+UiU/Yblq+xUtSImFu7cTbw==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/traverse@8.0.6': resolution: {integrity: sha512-ilYV+qLStGzSKLipJlETdvdtpK3rAuwfCmCoGKv15MU4EoeIuLkespSNoNLFGOPSciEHx9TLWtN5mv0fz5w5mQ==} engines: {node: ^22.18.0 || >=24.11.0} - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.8': resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} - '@babel/types@8.0.0': - resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/types@8.0.6': resolution: {integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==} engines: {node: ^22.18.0 || >=24.11.0} @@ -808,12 +750,12 @@ packages: '@blazediff/core@1.10.0': resolution: {integrity: sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==} - '@bramus/specificity@2.4.2': - resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + '@bramus/specificity@2.4.3': + resolution: {integrity: sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==} hasBin: true - '@bufbuild/protobuf@2.12.1': - resolution: {integrity: sha512-BvAMfS6LrgZiryOAZ4pBYucu4wG/Ei/9o9DZ9akbREnMLbPJiom2i8b9C8IsKErQoiKqVhrerzt3kOT/RrzLHg==} + '@bufbuild/protobuf@2.16.0': + resolution: {integrity: sha512-FWa0sPlqYGJgpTs6OxBRcL/AW4JT0OIO+W1ajerluoTVh8CV0B7XM1qsNCRgnkSCkRCgOICKHRQlY17jrsm0+Q==} '@bundled-es-modules/deepmerge@4.3.2': resolution: {integrity: sha512-q8doe7ndrY2IolUOFIn0A0++JBX38pMhN7kFhTF4cnjIcILf6X6H2yWczInyv8ZFdR0lrE8088X8XS5efxXz8A==} @@ -837,80 +779,67 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} + '@colors/colors@1.6.1': + resolution: {integrity: sha512-dTmUJzXSuayBK+hZydEaXd2mhx61qWQwkwaBBY6LyEOVx/L9aQU5ac8eFNEsd9nrD1+zb9zvDCphLSe8g1F4Qw==} + engines: {node: '>=0.1.90'} + '@csstools/color-helpers@6.1.2': resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==} engines: {node: '>=20.19.0'} - '@csstools/css-calc@3.3.0': - resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + '@csstools/css-calc@3.4.3': + resolution: {integrity: sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==} engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 - - '@csstools/css-calc@3.4.1': - resolution: {integrity: sha512-EtC7SoN1j6J4E4DCwg5QgbO5TGxgxIA1RXqe+W+qUM+BUcezx9wT+/tiQ/WO2yCX4i5X+Cuf9ciJ22aP4UEwWw==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.1 + '@csstools/css-parser-algorithms': ^4.0.2 '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-color-parser@4.2.4': - resolution: {integrity: sha512-DyefytAZ735mX4Dq/WcDAXFtXhaEFvme0ZS9tVEBAc2whxUthXr0R0L2rmEPm59SrMBkGrFzQviBXMs/UtnABQ==} + '@csstools/css-color-parser@4.2.6': + resolution: {integrity: sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==} engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.1 + '@csstools/css-parser-algorithms': ^4.0.2 '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-parser-algorithms@4.0.0': - resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} - engines: {node: '>=20.19.0'} - peerDependencies: - '@csstools/css-tokenizer': ^4.0.0 - - '@csstools/css-parser-algorithms@4.0.1': - resolution: {integrity: sha512-ShL8BqPfbKJrJiKFH0xBbN0i7Nrh9HXYRuF+pzyj93R/BL2YAsUxJeqANErzqM+0JGl7vjHp+3OIgd/DL69YIA==} + '@csstools/css-parser-algorithms@4.0.2': + resolution: {integrity: sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-syntax-patches-for-csstree@1.1.14': - resolution: {integrity: sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==} + '@csstools/css-syntax-patches-for-csstree@1.1.15': + resolution: {integrity: sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: css-tree: optional: true - '@csstools/css-tokenizer@4.0.0': - resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} - engines: {node: '>=20.19.0'} - '@csstools/css-tokenizer@4.0.2': resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} engines: {node: '>=20.19.0'} - '@csstools/media-query-list-parser@5.0.0': - resolution: {integrity: sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==} + '@csstools/media-query-list-parser@5.0.2': + resolution: {integrity: sha512-qx19O8AbbLLbDSAcI6YOby/Vq8rV51psgIvlF5OZT/L8fQJDCyQifL6k+kYU4a8ySpzJ4RBzZkIs530bAn8y4Q==} engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 - '@csstools/selector-resolve-nested@4.0.1': - resolution: {integrity: sha512-j3vdQu0XwLME5qOTWxm8cnmvsf423R2YL6DbKklCHZwkDm7UdKNu6RPlw4REIJhSlKBICY3B70/7QZdicLqZgg==} + '@csstools/selector-resolve-nested@4.0.2': + resolution: {integrity: sha512-JpjNBJFA+4OnAHKRK4AMp8xrYyLhI9XapOeBlP2PhRU/IylSw+1ep3BFNTksBJnrTS3KT6kqKNVfpzLLwZUW5Q==} engines: {node: '>=20.19.0'} peerDependencies: postcss-selector-parser: ^7.1.1 - '@csstools/selector-specificity@6.0.0': - resolution: {integrity: sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==} + '@csstools/selector-specificity@6.0.1': + resolution: {integrity: sha512-MBuqFIIvqk4S593MGAskTkPpEUUzZNvGzh8mMxEwl3oHwjQlXmqHEDxNbN3Y0Tsh+xjmem4GZjeg7I0qkwBivg==} engines: {node: '>=20.19.0'} peerDependencies: postcss-selector-parser: ^7.1.1 - '@dabh/diagnostics@2.0.8': - resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + '@dabh/diagnostics@2.0.9': + resolution: {integrity: sha512-R6siwR65Hm+3yfgP7o8DKhNvputQAwfoz9zTc3kyDudnomj2/BcLmD+uGQQPICjuFUp8ounPBU+jmKsocwVVAg==} '@emnapi/core@1.11.0': resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} @@ -918,27 +847,18 @@ packages: '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - '@emnapi/core@2.0.0-alpha.3': - resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} - '@emnapi/runtime@1.11.0': resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - '@emnapi/runtime@2.0.0-alpha.3': - resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} - '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@emnapi/wasi-threads@2.0.1': - resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} - '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} @@ -1095,46 +1015,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.21.2': - resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.5': - resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@exodus/bytes@1.15.1': - resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + '@exodus/bytes@1.16.0': + resolution: {integrity: sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: '@noble/hashes': ^1.8.0 || ^2.0.0 @@ -1162,26 +1044,6 @@ packages: '@hapi/topo@6.0.2': resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} - '@humanfs/core@0.19.2': - resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.8': - resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} - engines: {node: '>=18.18.0'} - - '@humanfs/types@0.15.0': - resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - '@internationalized/date@3.12.4': resolution: {integrity: sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==} @@ -1213,9 +1075,6 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/sourcemap-codec@1.6.0': resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} @@ -1258,50 +1117,50 @@ packages: peerDependencies: tslib: '2' - '@jsonjoy.com/fs-core@4.64.0': - resolution: {integrity: sha512-zs2TAq7Six5jgMuoMNjpspAvOP3mhtgq/k1UyQodEzCtQi/N83y2/y+zcvnZSGp/Rxq96DBN+bValOBQAyn/ew==} + '@jsonjoy.com/fs-core@4.80.0': + resolution: {integrity: sha512-qMKWshnyjbyhm+NbzqGE3w5y1mckFFEMj0aDPU2/JBNh1BzIjVy9esq9lBmRnNVmhMEkxwDzLZ8Gqh5G1P5Dag==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-fsa@4.64.0': - resolution: {integrity: sha512-nMWOVbkLFyEgmXZih3wyvxA9XpgyyqyfrINMHvEFqhi7uqfRl7c9ERJt6yX7vgMPrB9Uo+OJO+Spa0cFzPD01w==} + '@jsonjoy.com/fs-fsa@4.80.0': + resolution: {integrity: sha512-ZBEKl7J6dbkRCfmrFV48Re211A/FHqniTUhsWCsaRteNSOqMgkX3qF1UreVgwB9oSPm81JLAwhIi6Lb/NH/PoA==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-node-builtins@4.64.0': - resolution: {integrity: sha512-/o7WRFhUWaM/fOrslwLZGnzn4RmRILykn+lAL+mNObqqRNw+CQSiij6hpCeZ+C7buhdoVo7go/OYqzaSUfDYmA==} + '@jsonjoy.com/fs-node-builtins@4.80.0': + resolution: {integrity: sha512-OIOIhqaWiwUySFMny4epIAoviXsyMaQQ63PTc3zH+5HEOvarl5hpUmZIhp2m5+OfyKAh5ylxXnQIgew1OgJDVw==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-node-to-fsa@4.64.0': - resolution: {integrity: sha512-WDD9WVs0hb7UAEKTgZW2f66WDrbj7gIIWwpP3spbLyXa0rghtUaFTB8L4gdR3ZCWwiKIsj38/CNijpVmpnuPUw==} + '@jsonjoy.com/fs-node-to-fsa@4.80.0': + resolution: {integrity: sha512-ljeaR5xwtX1EyRmB0wvXANUMQZ3mjKcm7z8s2N8D8SLQ04CcAGtUhHFRZQXFb9mhgFyEVMydQGQtSOFS7pcrFQ==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-node-utils@4.64.0': - resolution: {integrity: sha512-k5Indsx9hWW9xSF7Y6oSKKwtCUNhzZxadub3owhIlitc+iMRVlPPdX2duTKQWBL3qNWpXya8jykgaaWpheeS4w==} + '@jsonjoy.com/fs-node-utils@4.80.0': + resolution: {integrity: sha512-xwHeVVi3f+Khg/Fmb8i5qoYVmQ08QznE5TWhQokUmIZjz4KBPzU+zt/+bahBI6G1ghtqajt27lWuAciIxApuzA==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-node@4.64.0': - resolution: {integrity: sha512-dO+NNkODbUli4uV42bcNrrLvq5rE7SNpdZ5TNd0dtbLsAaNK3MDiIC9lUi+brboGoIjW6vd2fB1qao60nrk5xA==} + '@jsonjoy.com/fs-node@4.80.0': + resolution: {integrity: sha512-yiBlUfkMMRFFGy8CD/h2zd5rPnrM6bjlF+nx2LRAxNV6hOSiuObQIUEE7m7y2VTMisjxCRtzINH94bFGySWL7g==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-print@4.64.0': - resolution: {integrity: sha512-PHZFccchvkhWrwPWHjmVAhbC3vSHCtyZvlZfJJ3ho2bnzl450hXri6/8e6pbkWdH+SkmLXNml0sV8e5HDAfxKw==} + '@jsonjoy.com/fs-print@4.80.0': + resolution: {integrity: sha512-1ocZLJw/m/xlq0moaIHO7HV9nHRVBUvdc2zMzhsJGkFOrqfwWvexfCv5cYCPPLRhP+9p3yunoi4Ep5YhRORC9Q==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' - '@jsonjoy.com/fs-snapshot@4.64.0': - resolution: {integrity: sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q==} + '@jsonjoy.com/fs-snapshot@4.80.0': + resolution: {integrity: sha512-3AV23Q1H228KTtJKTUE0nNq8FCtxSvtENOLrEat80wkAJCKHuqQEv8yjhpS8Bei2OEFJAnluGXp9Ri2SWBlROw==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' @@ -1357,25 +1216,12 @@ packages: '@types/react': '>=16' react: '>=16' - '@microsoft/api-extractor-model@7.32.2': - resolution: {integrity: sha512-Ussc25rAalc+4JJs9HNQE7TuO9y6jpYQX9nWD1DhqUzYPBr3Lr7O9intf+ZY8kD5HnIqeIRJX7ccCT0QyBy2Ww==} - - '@microsoft/api-extractor@7.56.2': - resolution: {integrity: sha512-d94f7S+H43jDxY/YIyp5UOE9N12HZmEPP5Ct96us+fw1FVKBoy4itTopdVM1VrcpduuA7fK/t31CgA2jM8AqSA==} - hasBin: true - - '@microsoft/tsdoc-config@0.18.1': - resolution: {integrity: sha512-9brPoVdfN9k9g0dcWkFeA7IH9bbcttzDJlXvkf8b2OBzd5MueR1V2wkKBL0abn0otvmkHJC6aapBOTJDDeMCZg==} - - '@microsoft/tsdoc@0.16.0': - resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} - - '@napi-rs/wasm-runtime@1.2.2': - resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} + '@napi-rs/wasm-runtime@1.2.5': + resolution: {integrity: sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -1522,11 +1368,8 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} - '@oxc-project/types@0.142.0': - resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} - - '@oxc-project/types@0.151.0': - resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==} + '@oxc-project/types@0.152.0': + resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} '@oxc-resolver/binding-android-arm-eabi@11.21.2': resolution: {integrity: sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==} @@ -1714,7 +1557,7 @@ packages: engines: {node: '>= 10.0.0'} '@penpot/svgo@https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021': - resolution: {gitHosted: true, tarball: https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021} + resolution: {gitHosted: true, integrity: sha512-hG/pgVEWhmHEFMU+evGZkB5kHauff5Zo6ZO+Ro7HY0efsQTJft6svM4isH5jDISeSVrZ1CDGnhWBXuqkztsTWw==, tarball: https://codeload.github.com/penpot/svgo/tar.gz/65b3a645df9edbe3c00acf4267dd06c2ca736021} version: 3.3.0 '@penpot/ua-parser@https://codeload.github.com/penpot/ua-parser/tar.gz/90b970f39f2dc08378b975a0f01045b4ec8e89a4': @@ -1814,192 +1657,98 @@ packages: resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} engines: {node: '>= 10'} - '@rolldown/binding-android-arm-eabi@1.2.11': - resolution: {integrity: sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==} + '@rolldown/binding-android-arm-eabi@1.2.12': + resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rolldown/binding-android-arm64@1.2.1': - resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} + '@rolldown/binding-android-arm64@1.2.12': + resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.2.11': - resolution: {integrity: sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.2.1': - resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} + '@rolldown/binding-darwin-arm64@1.2.12': + resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.2.11': - resolution: {integrity: sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.2.1': - resolution: {integrity: sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==} + '@rolldown/binding-darwin-x64@1.2.12': + resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.11': - resolution: {integrity: sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.2.1': - resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} + '@rolldown/binding-freebsd-x64@1.2.12': + resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.2.11': - resolution: {integrity: sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.2.1': - resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': + resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.2.11': - resolution: {integrity: sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.2.1': - resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} + '@rolldown/binding-linux-arm64-gnu@1.2.12': + resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.2.11': - resolution: {integrity: sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.2.1': - resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} + '@rolldown/binding-linux-arm64-musl@1.2.12': + resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.2.11': - resolution: {integrity: sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.2.1': - resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.12': + resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.2.11': - resolution: {integrity: sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.2.1': - resolution: {integrity: sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==} + '@rolldown/binding-linux-s390x-gnu@1.2.12': + resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.11': - resolution: {integrity: sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.2.1': - resolution: {integrity: sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==} + '@rolldown/binding-linux-x64-gnu@1.2.12': + resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.11': - resolution: {integrity: sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.2.1': - resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} + '@rolldown/binding-linux-x64-musl@1.2.12': + resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.2.11': - resolution: {integrity: sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.2.1': - resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} + '@rolldown/binding-openharmony-arm64@1.2.12': + resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.2.11': - resolution: {integrity: sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.2.1': - resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - - '@rolldown/binding-win32-arm64-msvc@1.2.1': - resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} + '@rolldown/binding-win32-arm64-msvc@1.2.12': + resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.2.11': - resolution: {integrity: sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.2.1': - resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.2.11': - resolution: {integrity: sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==} + '@rolldown/binding-win32-x64-msvc@1.2.12': + resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2016,177 +1765,6 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.61.1': - resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.61.1': - resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.61.1': - resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.61.1': - resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.61.1': - resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.61.1': - resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.61.1': - resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.61.1': - resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.61.1': - resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.61.1': - resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.61.1': - resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.61.1': - resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.61.1': - resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.61.1': - resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.61.1': - resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.61.1': - resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.61.1': - resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.61.1': - resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.61.1': - resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.61.1': - resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.61.1': - resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.61.1': - resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.61.1': - resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.61.1': - resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.61.1': - resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==} - cpu: [x64] - os: [win32] - - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - - '@rushstack/node-core-library@5.19.1': - resolution: {integrity: sha512-ESpb2Tajlatgbmzzukg6zyAhH+sICqJR2CNXNhXcEbz6UGCQfrKCtkxOpJTftWc8RGouroHG0Nud1SJAszvpmA==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - - '@rushstack/problem-matcher@0.1.1': - resolution: {integrity: sha512-Fm5XtS7+G8HLcJHCWpES5VmeMyjAKaWeyZU5qPzZC+22mPlJzAsOxymHiWIfuirtPckX3aptWws+K2d0BzniJA==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - - '@rushstack/rig-package@0.6.0': - resolution: {integrity: sha512-ZQmfzsLE2+Y91GF15c65L/slMRVhF6Hycq04D4TwtdGaUAbIXXg9c5pKA5KFU7M4QMaihoobp9JJYpYcaY3zOw==} - - '@rushstack/terminal@0.21.0': - resolution: {integrity: sha512-cLaI4HwCNYmknM5ns4G+drqdEB6q3dCPV423+d3TZeBusYSSm09+nR7CnhzJMjJqeRcdMAaLnrA4M/3xDz4R3w==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - - '@rushstack/ts-command-line@5.2.0': - resolution: {integrity: sha512-lYxCX0nDdkDtCkVpvF0m25ymf66SaMWuppbD6b7MdkIzvGXKBXNIVZlwBH/C0YfkanrupnICWf2n4z3AKSfaHw==} - '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -2197,28 +1775,28 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@storybook/addon-docs@10.6.0': - resolution: {integrity: sha512-Fk3rUQWeGyCfL170OkmRp6Ruyv7MHPb/xrKa6YNk7FlGmlVLi5tKzh2T/0J13vWEy8wiu2MV3IBu8iRsaUR+Cw==} + '@storybook/addon-docs@10.6.1': + resolution: {integrity: sha512-31PXR+MCWfI5YHKKah6tWnbV1FJWA9qbTHxtvpszPLd8FBekXtPOnmyYOpFF9CfciBF9bkeJFdFanJvUCjWbjA==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.6.0 + storybook: ^10.6.1 peerDependenciesMeta: '@types/react': optional: true - '@storybook/addon-themes@10.6.0': - resolution: {integrity: sha512-PJtRtaCzKCAcouEqkHevMwv0aeQNsYWab4K/qszNGuSyFXxdcPQguKwmbB6Ajd/FxW+Of/mLXjRgpjrGVSgm1g==} + '@storybook/addon-themes@10.6.1': + resolution: {integrity: sha512-SVZSM+mIiyK56d/3mRpRwPkAlpr9fdSSz/XpkT6fzI1PYFWDGSdElu2YmauzhaFGnlmAKL//tmXVvHXD4qCkIQ==} peerDependencies: - storybook: ^10.6.0 + storybook: ^10.6.1 - '@storybook/addon-vitest@10.6.0': - resolution: {integrity: sha512-GY2SwSXLNgyMqjQ59J7YTkEQTHdEbBMggOvbbuhuPUBhWo97OGSDCmn1UW3nXDZvkoWyWhF9T9628m1uei7FbA==} + '@storybook/addon-vitest@10.6.1': + resolution: {integrity: sha512-ZGB7kDd9o0UiXIIXLZMKK9ruNyqo2jEBT+2YjlpKGDtJR6iAEhfWyiPY8JkobCV5Fge0oBqyg1x9am4wv/vp5w==} peerDependencies: - '@vitest/browser': ^3.0.0 || ^4.0.0 - '@vitest/browser-playwright': ^4.0.0 - '@vitest/runner': ^3.0.0 || ^4.0.0 - storybook: ^10.6.0 - vitest: ^3.0.0 || ^4.0.0 + '@vitest/browser': ^3.0.0 || ^4.0.0 || ^5.0.0 + '@vitest/browser-playwright': ^4.0.0 || ^5.0.0 + '@vitest/runner': ^3.0.0 || ^4.0.0 || ^5.0.0 + storybook: ^10.6.1 + vitest: ^3.0.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: '@vitest/browser': optional: true @@ -2229,10 +1807,10 @@ packages: vitest: optional: true - '@storybook/builder-vite@10.6.0': - resolution: {integrity: sha512-7zt8tYB6dnSO8fbjUz91MHexGDpf9HrLlA3FAmlVb+2ZL3eWUs499L3I6BDUymcWu7oW5WCBJlB8ERj8ZLLzig==} + '@storybook/builder-vite@10.6.1': + resolution: {integrity: sha512-kUMRRRS4MpJdxOLhrlQX3cpMYi2vVB3tIIRXyqWmAjVM6xT7OZ1SCQEMFPP+S3ToRqaYWZQb8aBqrJucbbYufA==} peerDependencies: - storybook: ^10.6.0 + storybook: ^10.6.1 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 '@storybook/global@5.0.0': @@ -2243,40 +1821,40 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/react-dom-shim@10.6.0': - resolution: {integrity: sha512-ZE0l4xL6d00rKFeJhRDuh0t3r+/2ffhx47lIyaSWrTD5kuM3nDjKDxCIWwX4E3gLV+oNX8E1prXRMuEWJUJwJg==} + '@storybook/react-dom-shim@10.6.1': + resolution: {integrity: sha512-OkPFycR6kVE+9ZsgQWRqnUfZm/sLY6dxsNAn3lsvV7DSAQWH9QnHLkqP9E/N/Hg6cKPDtGzDZ8vPNDz2FuaDuQ==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.6.0 + storybook: ^10.6.1 peerDependenciesMeta: '@types/react': optional: true '@types/react-dom': optional: true - '@storybook/react-vite@10.6.0': - resolution: {integrity: sha512-nc1dqnmjGOcDJvPCkAn4Hmu1gi7dNHHoibNmlBp0d1adosejELZxuNjHBkPl/KxZjbzP3jljQHnHB/guxd8e2g==} + '@storybook/react-vite@10.6.1': + resolution: {integrity: sha512-oyUZxuW2lIJN8BRbYyMyZbfjeYqtF6HwmNfqa1SAF525zRnMewYp66wm/5d8FAgwqejOf6N2+wR3hX866lSwMA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.6.0 + storybook: ^10.6.1 typescript: '>= 4.9.x' vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - '@storybook/react@10.6.0': - resolution: {integrity: sha512-0xfYYPRHcr5ta1hmXJCSezH4JGuUHJ+xVF9f1j+Y/MWEMOQE4wnaKWKsk/HLrDjZkT5hKfvmogt9b0yduP6zEQ==} + '@storybook/react@10.6.1': + resolution: {integrity: sha512-t0QIzO0MVcpD52UEMZKSv35nO/+8vUnLGoFC1E3BIVjHEIdr6IFFM7ty2E7FWsaZGsah3XrFINKT3dPd2Atz+Q==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@types/react-dom': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.6.0 + storybook: ^10.6.1 typescript: '>= 4.9.x' peerDependenciesMeta: '@types/react': @@ -2312,8 +1890,8 @@ packages: '@types/react-dom': optional: true - '@testing-library/user-event@14.6.3': - resolution: {integrity: sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==} + '@testing-library/user-event@14.6.7': + resolution: {integrity: sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' @@ -2332,11 +1910,8 @@ packages: '@tokens-studio/types@0.5.2': resolution: {integrity: sha512-rzMcZP0bj2E5jaa7Fj0LGgYHysoCrbrxILVbT0ohsCUH5uCHY/u6J7Qw/TE0n6gR9Js/c9ZO9T8mOoz0HdLMbA==} - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - - '@types/argparse@1.0.38': - resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2371,17 +1946,11 @@ packages: '@types/jsesc@2.5.1': resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/mdx@2.0.14': resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==} - '@types/node@26.6.3': - resolution: {integrity: sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==} + '@types/node@26.6.4': + resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} '@types/react-dom@19.3.0': resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} @@ -2397,13 +1966,13 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - '@vitejs/plugin-react@6.1.1': - resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==} + '@vitejs/plugin-react@6.1.2': + resolution: {integrity: sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 babel-plugin-react-compiler: ^1.0.0 - oxc-transform-react: ^0.145.0 + oxc-transform-react: ^0.152.0 vite: ^8.0.0 peerDependenciesMeta: '@rolldown/plugin-babel': @@ -2413,22 +1982,22 @@ packages: oxc-transform-react: optional: true - '@vitest/browser-playwright@5.0.2': - resolution: {integrity: sha512-wU0B1Zy9+a55fdUT7JBCcnnjpTx0HHvUlQT6yFnuwCSY244BkPPwabNX+9PAljc96gRELcfm5APtnsn/3qiaPw==} + '@vitest/browser-playwright@5.0.3': + resolution: {integrity: sha512-rYBHwIFL/ZtI6dl0rbrAatScXCrvX+TL2BX2AGOigkC1Y6u3rIWw8KMQJJMS4drSa1ksMCmZU2wpgwdE7kqu3g==} peerDependencies: playwright: 1.62.1 - vitest: 5.0.2 + vitest: 5.0.3 - '@vitest/browser@5.0.2': - resolution: {integrity: sha512-nX9xXZ/j8YfU7KTeFa+tpSP331SbI81qBQTASRlcopYeaTgBerNmJryzHXHrFOW0NYldd3Dn5gw651zGnmaQzw==} + '@vitest/browser@5.0.3': + resolution: {integrity: sha512-7KHIkVA/gTRxQV9qPa9qLK+g5INZG6iRrlwXSFG5EKsOomW3UzzW7wsdt1Yp5PufzQ43lniuONlb4lUVCC2qSg==} peerDependencies: - vitest: 5.0.2 + vitest: 5.0.3 - '@vitest/coverage-v8@5.0.2': - resolution: {integrity: sha512-3ffHBEi8DOOBLwIGBhOBZbRfYFYWjMUuxZicONdhuFEFEG5AVsMOSrVeuROskqnqpbOmEJwxM2eTVZ9N3a1t+A==} + '@vitest/coverage-v8@5.0.3': + resolution: {integrity: sha512-+klsyz7BvT1vCU28Zkfzms1Ia78XD0V41U3FUtRaa3S+vOr/EXvK1O6BvVD7l1RzEjm6SONR2aybOvDDf9u0mQ==} peerDependencies: - '@vitest/browser': 5.0.2 - vitest: 5.0.2 + '@vitest/browser': 5.0.3 + vitest: 5.0.3 peerDependenciesMeta: '@vitest/browser': optional: true @@ -2444,8 +2013,8 @@ packages: resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==} engines: {node: '>=22'} - '@vitest/mocker@5.0.2': - resolution: {integrity: sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==} + '@vitest/mocker@5.0.3': + resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2458,34 +2027,25 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - - '@vitest/pretty-format@5.0.2': - resolution: {integrity: sha512-YHM+mQQ7N1ROHMugJ7P/M+fC/q1G4zs/iAMZZCvhHcY8WkwPQcXBHD+z18oz+808Cfa12K7jOr3XPQMcLIovjw==} - - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + '@vitest/pretty-format@5.0.3': + resolution: {integrity: sha512-3hr5eV5z2ff0l2Q1vaKQbB4j3M3tElw8stDokEOTqXJB3Es5VrT8gBRIMY59gGuFp8Y61sWlWFK1aNUH5dHDYw==} '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - '@vitest/spy@5.0.2': - resolution: {integrity: sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==} + '@vitest/spy@5.0.3': + resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==} - '@vitest/ui@5.0.2': - resolution: {integrity: sha512-FRGWhLK/S+aaAtty/5d+jTtY5l+9pGNcjz6IHbyUiP3VWs6a+zIgU4lc1SzWCR7H+OXgetAI06i/VpCWQmVorQ==} + '@vitest/ui@5.0.3': + resolution: {integrity: sha512-9dfvqDBvSbHM6y9YVXxAypkl2z7mrWozK/JFo237qI0cj/3M/kzDKiVZqzi3TE54Ler7Jditud7OMBZCyUvRHA==} peerDependencies: - vitest: 5.0.2 + vitest: 5.0.3 '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - - '@vitest/utils@5.0.2': - resolution: {integrity: sha512-mgpUtxFhKeOKVaArBVHNJGdYHAeRh135o59l13uVmPUCS1OUtis5pBqXEgE25iqRgFqEBwXvJlkhknRsYnMeqg==} + '@vitest/utils@5.0.3': + resolution: {integrity: sha512-77fDTZv8eu1CzlIfgPU2yrl5MXSUaUAOvgdZEeVm3X3WIxUubIX2SforqS6176lT32HCF6uBVMYVqfHxw7o6eg==} '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -2504,8 +2064,8 @@ packages: '@webcontainer/env@1.1.1': resolution: {integrity: sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==} - '@xmldom/xmldom@0.8.13': - resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} engines: {node: '>=10.0.0'} '@zip.js/zip.js@2.8.34': @@ -2520,13 +2080,8 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.18.0: - resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} engines: {node: '>=0.4.0'} hasBin: true @@ -2534,28 +2089,6 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} - ajv-draft-04@1.0.0: - resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} - peerDependencies: - ajv: ^8.18.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.18.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv@6.15.0: - resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -2563,8 +2096,8 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + ansi-regex@6.4.0: + resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==} engines: {node: '>=12'} ansi-styles@3.2.1: @@ -2587,9 +2120,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2604,40 +2134,16 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - arkregex@0.0.5: - resolution: {integrity: sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==} + arkregex@0.0.12: + resolution: {integrity: sha512-sOxL0eX50pnRYO4OyajKZChGuuj/iHwC5tysiAaGOPBu+zJwJQckWn7/DRH2++ol5uaKpyRn7N0hKT7nDqGmgg==} - arktype@2.1.29: - resolution: {integrity: sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==} + arktype@2.2.7: + resolution: {integrity: sha512-AbgM5SQvwCTURSR+ENfPtOG7cL5VZ0hqOVtoWyXKw2SviFddZhCBEUAjjVJYKCNFlPPR1VX5LGUKxsSqqc4Kdw==} array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - - array.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} - - array.prototype.findlastindex@1.2.6: - resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} - arraybuffer.prototype.slice@1.0.4: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} @@ -2652,15 +2158,12 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-types-flow@0.0.8: - resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - - ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + ast-types@0.16.3: + resolution: {integrity: sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==} engines: {node: '>=4'} - ast-v8-to-istanbul@1.0.5: - resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} + ast-v8-to-istanbul@1.0.7: + resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==} astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} @@ -2687,16 +2190,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.11.1: - resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} - engines: {node: '>=4'} - - axios@1.19.0: - resolution: {integrity: sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} + axios@1.20.0: + resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==} babel-plugin-react-compiler@1.0.0: resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} @@ -2711,13 +2206,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.11.26: - resolution: {integrity: sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - baseline-browser-mapping@2.11.8: - resolution: {integrity: sha512-zAgkquC2WYF0PIc6XbNYkA2uuxxFavzgmX61R+dHDUa558V8Ejf8ozTZFR6QzM24RWu4kBcRkhJ5kpz77j9fnQ==} + baseline-browser-mapping@2.11.27: + resolution: {integrity: sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -2745,24 +2235,19 @@ packages: resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==} engines: {node: '>=20.19.0'} - brace-expansion@1.1.18: - resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.7: - resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - browserslist@4.29.2: - resolution: {integrity: sha512-/u9r8k8ue4ZhHCw9ovDtltpWdXeXzjhdxCGj6vm1RGLPXb8lu33EMdoCHF5Sx8oYBS+Q/FYYNmAqlHncxR3RmA==} + browserslist@4.29.3: + resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2775,8 +2260,8 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - bundle-name@4.1.0: - resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + bundle-name@4.1.1: + resolution: {integrity: sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==} engines: {node: '>=18'} bytes@3.1.2: @@ -2802,11 +2287,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001806: - resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} - - caniuse-lite@1.0.30001813: - resolution: {integrity: sha512-zfjJo4rM0+fUomGDBW/xcDjhIwz/210DGvip2MAMDZ8KHcRPnOHmEgHPZP0UHlZoxr8fYKVJOqcORhYQcG4FKQ==} + caniuse-lite@1.0.30001814: + resolution: {integrity: sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==} canvas@3.2.3: resolution: {integrity: sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==} @@ -2816,18 +2298,14 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} engines: {node: '>=18'} chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -2868,6 +2346,7 @@ packages: clone-buffer@1.0.0: resolution: {integrity: sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==} engines: {node: '>= 0.10'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. clone-stats@1.0.0: resolution: {integrity: sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==} @@ -2904,8 +2383,8 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-name@2.1.0: - resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + color-name@2.1.1: + resolution: {integrity: sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==} engines: {node: '>=12.20'} color-string@2.1.4: @@ -2971,22 +2450,22 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - confbox@0.2.4: - resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + confbox@0.3.1: + resolution: {integrity: sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==} config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} - content-disposition@1.0.1: - resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} convert-source-map@2.0.0: @@ -3000,8 +2479,8 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - core-js-pure@3.48.0: - resolution: {integrity: sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==} + core-js-pure@3.50.0: + resolution: {integrity: sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3022,10 +2501,6 @@ packages: resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} engines: {node: '>=4.8'} - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - css-functions-list@3.3.3: resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==} engines: {node: '>=12'} @@ -3082,12 +2557,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - - data-urls@7.0.0: - resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-urls@8.0.0: + resolution: {integrity: sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==} + engines: {node: ^22.14.0 || >=24.0.0} data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} @@ -3112,14 +2584,6 @@ packages: supports-color: optional: true - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -3144,9 +2608,6 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -3155,8 +2616,8 @@ packages: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} - default-browser@5.5.0: - resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} engines: {node: '>=18'} define-data-property@1.1.4: @@ -3194,14 +2655,6 @@ packages: dettle@1.0.5: resolution: {integrity: sha512-ZVyjhAJ7sCe1PNXEGveObOH9AC8QvMga3HJIghHawtG7mE4K5pW9nz/vDGAr/U7a3LWgdOzEE7ac9MURnyfaTA==} - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} - engines: {node: '>=0.3.1'} - - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -3263,11 +2716,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.399: - resolution: {integrity: sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==} - - electron-to-chromium@1.5.439: - resolution: {integrity: sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==} + electron-to-chromium@1.5.446: + resolution: {integrity: sha512-kWlGMPHzuitCKx0O+MbY1ZGvQmP14JtckrnGjD4JsusTxh2cX9wu4bb4rOYZqLSvENjFFrHzlqY8kXhseucR/w==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3275,11 +2725,8 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - empathic@2.0.1: - resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + empathic@2.1.0: + resolution: {integrity: sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==} engines: {node: '>=14'} enabled@2.0.0: @@ -3298,8 +2745,8 @@ packages: entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - entities@8.0.0: - resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} engines: {node: '>=20.19.0'} env-paths@2.2.1: @@ -3309,8 +2756,12 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-abstract@1.24.1: - resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -3321,27 +2772,19 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-iterator-helpers@1.2.2: - resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} - engines: {node: '>= 0.4'} - es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} engines: {node: '>= 0.4'} esbuild@0.28.2: @@ -3360,106 +2803,11 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-import@2.32.0: - resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - - eslint-plugin-jsx-a11y@6.10.2: - resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - - eslint-plugin-react-hooks@7.1.1: - resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} - engines: {node: '>=18'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 - - eslint-plugin-react@7.37.5: - resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -3498,8 +2846,8 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - exsolve@1.1.0: - resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + exsolve@1.1.3: + resolution: {integrity: sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==} fancy-log@2.0.0: resolution: {integrity: sha512-9CzxZbACXMUXW13tS0tI8XsGGmxWzO2DmYrGuBJOJ8k8q2K7hwfJA5qHjuPPe8wtsco33YR9wc+Rlr5wYFvhSA==} @@ -3512,21 +2860,15 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fast-uri@3.1.5: - resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} fastest-levenshtein@1.0.16: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} fbjs-css-vars@1.0.2: resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} @@ -3552,10 +2894,6 @@ packages: file-entry-cache@11.1.5: resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -3564,14 +2902,6 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - flat-cache@6.1.23: resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} @@ -3585,8 +2915,8 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - follow-redirects@1.16.0: - resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + follow-redirects@1.16.1: + resolution: {integrity: sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -3616,10 +2946,6 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@11.3.6: - resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} - engines: {node: '>=14.14'} - fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -3636,8 +2962,8 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} engines: {node: '>= 0.4'} functions-have-names@1.2.3: @@ -3658,8 +2984,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.6.0: - resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} engines: {node: '>=18'} get-intrinsic@1.3.0: @@ -3688,12 +3014,8 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob-to-regex.js@1.2.0: - resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} + glob-to-regex.js@1.3.1: + resolution: {integrity: sha512-zhWhMRsgnOym+1txhUmYJGD6eUsbUdrZv55gCYKuJX8t2p3+M8My23PD09bEIsreGUs5pnSa/idmcfp2/IlqGg==} engines: {node: '>=10.0'} peerDependencies: tslib: '2' @@ -3717,10 +3039,6 @@ packages: resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} engines: {node: '>=6'} - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -3778,12 +3096,6 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} - hermes-estree@0.25.1: - resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} - - hermes-parser@0.25.1: - resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - highlight.js@11.12.0: resolution: {integrity: sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==} engines: {node: '>=12.0.0'} @@ -3821,8 +3133,8 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} icss-utils@5.1.0: @@ -3837,16 +3149,12 @@ packages: ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + ignore@7.0.12: + resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==} engines: {node: '>= 4'} - ignore@7.0.6: - resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} - engines: {node: '>= 4'} - - immutable@3.8.3: - resolution: {integrity: sha512-AUY/VyX0E5XlibOmWt10uabJzam1zlYjwiEgQSDc5+UIkFNaF9WM0JxXKaNMGf+F/ffUF+7kRKXM9A7C0xXqMg==} + immutable@3.8.4: + resolution: {integrity: sha512-ZQv17KolYrYmsDoDB8N67zhIKsNi9mGYlk96y/yuxyAqJB2hLzSGSM7k/sB0/ZvO4kx27U9+fBeD/XlLGh/uXQ==} engines: {node: '>=0.10.0'} immutable@5.1.9: @@ -3856,17 +3164,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-lazy@4.0.0: - resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} - engines: {node: '>=8'} - import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -3923,8 +3223,8 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} engines: {node: '>= 0.4'} is-data-view@1.0.2: @@ -3940,6 +3240,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -4059,15 +3363,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - iterator.prototype@1.1.5: - resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} - engines: {node: '>= 0.4'} - - jju@1.4.0: - resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - - joi@18.2.3: - resolution: {integrity: sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==} + joi@18.2.9: + resolution: {integrity: sha512-2mD929bUVKUhOLQQEVhlf6EZ0Mlo0DeRb5MO7cViR9AXLtBauuccEtB1py9Ocxpo/P7ucnh442iY/iOwrh3IQw==} engines: {node: '>= 20'} js-beautify@2.0.3: @@ -4084,12 +3381,12 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true - jsdom@30.1.1: - resolution: {integrity: sha512-FahmoPK5vbPc+jxV1iErMHmAZypCZ942NHF4+qqaWAuvaKKTBZxawnmAtrbGWLU7MtlxfqIP0qw6aSI+aWGtLg==} + jsdom@30.1.2: + resolution: {integrity: sha512-0FFE/jE1rppmVfUrJUgxqXjcwolZYIGtAgj1pTussMhNZxIxi7W/PvfWaMNroGi2B36X1IKHbexpZ9DhkoOPiQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} peerDependencies: canvas: ^3.2.3 @@ -4102,28 +3399,15 @@ packages: engines: {node: '>=6'} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-better-errors@1.0.2: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -4132,16 +3416,6 @@ packages: jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - jsonfile@6.2.1: - resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} - - jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - keyv@5.6.0: resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} @@ -4158,17 +3432,6 @@ packages: kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - language-subtag-registry@0.3.23: - resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} - - language-tags@1.0.9: - resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} - engines: {node: '>=0.10'} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} @@ -4258,10 +3521,6 @@ packages: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -4291,10 +3550,6 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@11.5.2: - resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} - engines: {node: 20 || >=22} - lru-cache@11.5.3: resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} engines: {node: 20 || >=22} @@ -4302,10 +3557,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -4313,17 +3564,17 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magic-string@1.4.2: - resolution: {integrity: sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==} + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} - magicast@0.5.4: - resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} map-stream@0.0.7: resolution: {integrity: sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==} - marked@18.0.14: - resolution: {integrity: sha512-mBHK6FBHuBAlhgRe88w9F0O1AbwwXJUcQibUbC/QcdTbVGAD7aWza+xt3N6oT/jCZx3/OMeS+8rnuiHZcQ9s7A==} + marked@18.1.0: + resolution: {integrity: sha512-PamYXWWWg2nboG3oX5Ffzpy3EFZROqZJK9hSlMmUnekp7TxPs5DcT95vwd46m73/exfKCRVMtgXoPMI4FEJL6w==} engines: {node: '>= 20'} hasBin: true @@ -4343,14 +3594,12 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} engines: {node: '>= 0.8'} - memfs@4.64.0: - resolution: {integrity: sha512-Kw72fgY7Wn+sD8KmtNWSafl1dz0UvAsE/PHs3YVfLiaZuA3HxNm9sRLqAu0ATiBGJvME1PxZXbBZPv5GycDeAw==} - peerDependencies: - tslib: '2' + memfs@4.80.0: + resolution: {integrity: sha512-N70W2isi44XdGn7uPo9NJ4zRukcdkwSFHlnYx42SWOH/+hzqNc8P7+tVoA2up4z7faHvNi1RSNCFjxoYQ71VPQ==} memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} @@ -4396,8 +3645,8 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -4435,30 +3684,27 @@ packages: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@0.6.4: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - node-abi@3.93.0: - resolution: {integrity: sha512-Cu6yUpX5Iavugm8BeX7c0wgU9CvOqfd1yM6A1d2q2ZMjym7GjpASv2GdRcTq3Fx+Sb5OgBkEEpw4VnAbY6Y5RA==} + node-abi@3.97.0: + resolution: {integrity: sha512-6Pu8F0eXGZlyz0EepPJrWgWDcBI8rKt2Wj8exOJU1tjVbu/VyZy5ZaLYS5N+Jp/Y3IP3sIRKcIOElTu7uC6KKA==} engines: {node: '>=10'} node-addon-api@7.1.1: @@ -4473,10 +3719,6 @@ packages: encoding: optional: true - node-releases@2.0.51: - resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} - engines: {node: '>=18'} - node-releases@2.0.57: resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} engines: {node: '>=18'} @@ -4530,24 +3772,8 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} - object.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} on-finished@2.4.1: @@ -4572,12 +3798,8 @@ packages: resolution: {integrity: sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A==} hasBin: true - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + own-keys@1.0.2: + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} engines: {node: '>= 0.4'} oxc-parser@0.127.0: @@ -4587,18 +3809,10 @@ packages: oxc-resolver@11.21.2: resolution: {integrity: sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - p-limit@7.3.3: resolution: {integrity: sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==} engines: {node: '>=20'} - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -4624,10 +3838,6 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -4636,10 +3846,6 @@ packages: resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} engines: {node: '>=4'} - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -4680,10 +3886,6 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - picomatch@4.0.7: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} @@ -4700,8 +3902,8 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pkg-types@2.3.1: - resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + pkg-types@2.3.3: + resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} playwright-core@1.62.1: resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} @@ -4765,8 +3967,8 @@ packages: postcss-resolve-nested-selector@0.1.6: resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} - postcss-safe-parser@7.0.1: - resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + postcss-safe-parser@7.1.0: + resolution: {integrity: sha512-1WzZxRLaAFwEh6Do+zyGpjWV3nGJNxxhuh7Ubu/q1ICImMgZJnLwhoaEpRbG4pJppp2Y1ncL9ffA2f+LhrefQg==} engines: {node: '>=18.0'} peerDependencies: postcss: ^8.5.10 @@ -4777,10 +3979,6 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss-selector-parser@7.1.4: - resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} - engines: {node: '>=4'} - postcss-selector-parser@7.1.6: resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} engines: {node: '>=4'} @@ -4795,8 +3993,8 @@ packages: resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} engines: {node: '>=6.0.0'} - postcss@8.5.28: - resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + postcss@8.5.29: + resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==} engines: {node: ^10 || ^12 || >=14} prebuild-install@7.1.3: @@ -4805,10 +4003,6 @@ packages: deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - prettier@3.9.9: resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} engines: {node: '>=14'} @@ -4847,8 +4041,8 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} proxy-from-env@2.1.0: @@ -4872,8 +4066,8 @@ packages: resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} engines: {node: '>=20'} - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} quansync@0.2.11: @@ -4885,8 +4079,8 @@ packages: randomcolor@0.6.2: resolution: {integrity: sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} raw-body@3.0.2: @@ -4919,8 +4113,8 @@ packages: peerDependencies: typescript: '>= 4.3.x' - react-docgen@8.0.3: - resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==} + react-docgen@8.0.4: + resolution: {integrity: sha512-gIYjtVTU4NCWZnb/LXVSWfClZXnjzX3+xxi69Tkfu4YsmYuqiOk8giLkKt/WwxWqh+U66CTh8Kqbha1uPv8/qQ==} engines: {node: ^20.9.0 || >=22} react-dom@19.3.0: @@ -4976,16 +4170,16 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} readline-sync@1.4.10: resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==} engines: {node: '>= 0.8.0'} - recast@0.23.19: - resolution: {integrity: sha512-T98lym7kH+pnZmRaD8yDRdaNqyUbwnbEBx0MuchrzMFOEMray4AO3ZJoTUZ5r78Ao78X/OhzW0DL8GB85w/I2w==} + recast@0.23.21: + resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==} engines: {node: '>= 4'} redent@3.0.0: @@ -5024,10 +4218,6 @@ packages: engines: {node: '>= 0.4'} hasBin: true - resolve@2.0.0-next.5: - resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} - hasBin: true - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5037,21 +4227,11 @@ packages: engines: {node: 20 || >=22} hasBin: true - rolldown@1.2.1: - resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} + rolldown@1.2.12: + resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.2.11: - resolution: {integrity: sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - rollup@4.61.1: - resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -5069,8 +4249,8 @@ packages: rxjs@8.0.0-alpha.14: resolution: {integrity: sha512-oRCzFwbAMbo0+dVUeGUCCEf339mW7CESFvVDG3RTncD6yMtV2XoubFMpKEhBd2a9d04EpHl8QSz84/V/agG6bw==} - safe-array-concat@1.1.3: - resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} safe-buffer@5.1.2: @@ -5094,125 +4274,125 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sass-embedded-all-unknown@1.105.0: - resolution: {integrity: sha512-+nLT5FtIqB5uDTqvpOi0RR7sT6eZFuE1EKgchg4kpGa3TrKfoinpBoqHzNi9jIJqVaP+AEEXavTolsUHVXR8wg==} + sass-embedded-all-unknown@1.105.1: + resolution: {integrity: sha512-kN0G03K9KoUbSf72t7ZvxAudcB4Wbf0M0XFaPc1gaF94jEPUb0xLCQUIPIDPH2Kx8vpYZ/GzzgF7USsB6rUYfw==} cpu: ['!arm', '!arm64', '!riscv64', '!x64'] - sass-embedded-android-arm64@1.105.0: - resolution: {integrity: sha512-lJzSYBBkl27KCAglQ2J2kYj6q0cxZP6n90XXH2SwAX6eFfYPhZTSbjcOf940pfEmxVq3W9HmyvhdU9FDz+lSBQ==} + sass-embedded-android-arm64@1.105.1: + resolution: {integrity: sha512-lWnIFmW3gL4ZDfZgjeUfxazmA0zlwSS/ADe2q9XmP0W2UfM9kuvrXdWvp6KjZOCv0CrTYVQMrIUj8Of1L2M24Q==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [android] - sass-embedded-android-arm@1.105.0: - resolution: {integrity: sha512-U64yUn0Gq/iHc9l0MBkrNtHgw7dLBEWGYTOvaD3OhQQv/2GG2Xj/63F5QN8VDPKZZQL43RRiHVX7AbAW3atsLQ==} + sass-embedded-android-arm@1.105.1: + resolution: {integrity: sha512-WYltUXkUP9jDaUq4uUiLgZXyYO5c72sa/TvpO4E2MMRdS+WJWgrPUDyvO+DpMR9gKWIm0QQ5cCEYnPDgovjVJw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [android] - sass-embedded-android-riscv64@1.105.0: - resolution: {integrity: sha512-QtGPt15dx6bWhnqqDZ5kmj8UmYVMkGCGgSRwa3johwioqqTf+kkomyqNod9ILiXAI4PVIZ4le+9HQwfjwOmUkQ==} + sass-embedded-android-riscv64@1.105.1: + resolution: {integrity: sha512-lWity0qhyQyPgkHUfZz+4IeD0r6mcAqG9KzOwVSjUrcSR9xAfWFKYAtHTA05R28ymyXf2Sf2+XfNV7YkxLgkUQ==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [android] - sass-embedded-android-x64@1.105.0: - resolution: {integrity: sha512-5+IO3LTvYLV06QX8CMCGkhby9uMzV3rcR6WPSW4H7ueGqJGQ2VUNuQG44q9GIs6+Ss06Zz5wgiohyYICdI6Cpw==} + sass-embedded-android-x64@1.105.1: + resolution: {integrity: sha512-FRGZuIZ9eS3qutYPFqz8u12Ti9ccyWKWb7F7rKRI1BkIEmLZV1rC862dUGnTV2d9Tk9qbSAXL71t8alUR+fzIA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [android] - sass-embedded-darwin-arm64@1.105.0: - resolution: {integrity: sha512-pdBHxweZVa10ACFOGbrcejHhHDXxuJOu31HAc5/Hcw0cZGm4H2J/gF+6mTttjZqhjCIAQFiGR9Y6cQHt39NtGA==} + sass-embedded-darwin-arm64@1.105.1: + resolution: {integrity: sha512-75qVpHj0qwJKWOo+ZA5NPVFnOGfRrUiBdnLR0dPqsvzp3acvb5gBevptqTtJjVWubpYRIjcRdDDjmeWoXnfacQ==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [darwin] - sass-embedded-darwin-x64@1.105.0: - resolution: {integrity: sha512-AS4Inw2bIntl9y1AbfoRwJLCAXqRAIvjl5iuQsKF/SSHvAf6feuKnUsxLBqFoT1/Ohr6eZte48qyth8XZ7DRQQ==} + sass-embedded-darwin-x64@1.105.1: + resolution: {integrity: sha512-5TV8xyZrOO89ky752favkTuyz9O2hOB2aMoiUpMTk0rFr+UVANcXdIrEvJ9WKoqTHaszim7un3jADumunpW7zQ==} engines: {node: '>=14.0.0'} cpu: [x64] os: [darwin] - sass-embedded-linux-arm64@1.105.0: - resolution: {integrity: sha512-vwLA8YYmrYm+ro5EQkvqwf3qEsxfjNf5Wx0W1wTCLq3fqQbynR2qc5siVxxNKJ6lHKUAxDMPtJ8lwdPYkeW0Uw==} + sass-embedded-linux-arm64@1.105.1: + resolution: {integrity: sha512-arALlVQkXjxN1RPaQR41vbDYXrTIdXwPtEzBWRkOg7FdIN/dSohY8ik96M6M3MSc+niV/UIC7BO47PcspzJGpw==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] libc: glibc - sass-embedded-linux-arm@1.105.0: - resolution: {integrity: sha512-YlE2SVdlDpYKOchX69DORYyBL/jctibVrYLUvG+5JITkhajsWOG0RPr4aZHeBQScnkj1UoPOReFp9wxjoej4eA==} + sass-embedded-linux-arm@1.105.1: + resolution: {integrity: sha512-Ng9GWIF6heTrwc9r/FFY9oUSoaylV2zcS296Qo2re3tJfLMWR37O2JUMyzPT+vtkRs8k3JOE6jTFVlXYnz4EAA==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] libc: glibc - sass-embedded-linux-musl-arm64@1.105.0: - resolution: {integrity: sha512-Y2Dxx6fDlAewr9Mmb8yezeaWczuF4H0FjbpdGjdzGeWcslokB357yAD2Rd6bi0IHXcQpQ8hRHnQujdJC+dbpSg==} + sass-embedded-linux-musl-arm64@1.105.1: + resolution: {integrity: sha512-KizYVR3d70U67iLgKdF+HCKBdQYUxYiv0YWCS3zl6GwNGIbGXlkOtnjxus41CKTA4oOK0KsSus9P5zwhOBjiYA==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] libc: musl - sass-embedded-linux-musl-arm@1.105.0: - resolution: {integrity: sha512-0ihXQb6YN8xWWSxtFUOEFrsAB/qYGPcHq0n8BDxuQUIopDdjBYPfhM92ySpggsJaEceGY8OSlj6voYbo2BmW5A==} + sass-embedded-linux-musl-arm@1.105.1: + resolution: {integrity: sha512-7u932XawHxGZvuYV4pOqcCvQi05yMONDmNDxVNh8Q7KmpytQP7mnyxBOYWStiw/p665AVL6BYGe7WY/vzCcWGw==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] libc: musl - sass-embedded-linux-musl-riscv64@1.105.0: - resolution: {integrity: sha512-ga/t+kLxkYFqTSstgGuTqhg9Wgwj2stfe4gqYk4YV/Q5AVi6ij9a0N/nsFU6eP9sNWW61zH7HDK/NTt+IR8PLw==} + sass-embedded-linux-musl-riscv64@1.105.1: + resolution: {integrity: sha512-gs84BgRBVNMX5fHRU3wxc3HuJjl0dUmZFpcVIaDDEVhnp5oXFLQp+gEu0xBBAurTzCHVpPU6GlGK6B1gRKmbhg==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] libc: musl - sass-embedded-linux-musl-x64@1.105.0: - resolution: {integrity: sha512-Pk5WXQyg3lsklItY69qL39j7Nzm8T/W9Cm2jho2h/ZSH0NDxNoDBlKunxiCOdjPEQMhtzXhI0KQN3wgkbfPlWg==} + sass-embedded-linux-musl-x64@1.105.1: + resolution: {integrity: sha512-jFSRc1BSoCo0bw6GXmBWQMzdj0FBl2FTKSRqQSye3vmlI9O466DwBK4OQL9HRp6MGdY9IQFFMZLPZo5LJWIPsQ==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] libc: musl - sass-embedded-linux-riscv64@1.105.0: - resolution: {integrity: sha512-LM6wuR0PoJjBz7iO9XRjRnyliWK46d8NnJrPI/uOr4ONT64xtk7yZzSl9ZpVgoEVhkJbBOK3p+AgTEBGi1xvdQ==} + sass-embedded-linux-riscv64@1.105.1: + resolution: {integrity: sha512-tkZs6OvA+omJgSf8EaPQavq6M7bJNLm88eKt3yzYnBy0dVGMao4+lyRb/EnsgIq2R/B2lhxUHhSVNWAcY5bgXw==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] libc: glibc - sass-embedded-linux-x64@1.105.0: - resolution: {integrity: sha512-1hsXWE1lxGqk1WHPT019y4+CU6JTrKtyQqTNOrvhsOb8OLZMrcfbViL65aU7qxfSzaYvGxT27d9qsZwQ63CCyA==} + sass-embedded-linux-x64@1.105.1: + resolution: {integrity: sha512-YF4OyymSlm26OcdPovIZL1blzjTxf5yN/rzgTmoGqAjEk57+yIuYo+7jBCgSQZFov8Rn4uHhZT+u0cpzxNFMRg==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] libc: glibc - sass-embedded-unknown-all@1.105.0: - resolution: {integrity: sha512-Bvipiu2fsTa6Xz1vJGNojFwgUeRmMOEeJmg1GjzYYe9dTqe94ookSF7CVlq9Naa5DT2jKtb9BKjwYv49j7c6hw==} + sass-embedded-unknown-all@1.105.1: + resolution: {integrity: sha512-bd2zDf+MdoPdRDH1awSQ+IL67vHvCbiKsWJ95XchPg3I67zQlXjTSf9KqRiE6twjNT1fRV82JUKoc0Hf6CUmfQ==} os: ['!android', '!darwin', '!linux', '!win32'] - sass-embedded-win32-arm64@1.105.0: - resolution: {integrity: sha512-zLh4GujaEa6njnjGqsThLhIK5Hy7Wp/8Xwt0DZ1Q5mfpW8Qp1vvV6J6J7sKeHmp5kav7rYr8bTX0LCoFwqIgGQ==} + sass-embedded-win32-arm64@1.105.1: + resolution: {integrity: sha512-Zpp/RWYL8ZzpLzBrs+/UVMEHOHx7sw4nlq2R52dKQ0qv/0V8oKemAy41PZyLir3c+S2C+YViYyfnFfQLRTAzuA==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [win32] - sass-embedded-win32-x64@1.105.0: - resolution: {integrity: sha512-nY9vFO9eM03o00I6bAGE/NkSDfBuIdB3tRuRyzw9jC5syYQunnCZ1GduBNDTOQlYncoNz0JQQNTe79DyjqeChg==} + sass-embedded-win32-x64@1.105.1: + resolution: {integrity: sha512-DM8EkMDzEsxnYoy11nyA5I4jr8lOV1TVQ/drNYvFQA9undiMzZD+7SkRA0OnCrgD1uCT+uRArCHSpNiiYVks+g==} engines: {node: '>=14.0.0'} cpu: [x64] os: [win32] - sass-embedded@1.105.0: - resolution: {integrity: sha512-5tctFPV7AvCT2bhXAUH+5ro074g7HjYlXsGR1VkCMdoXMchoSYTpPraDuRX7LAIFwyJAESTRGfaNd15eAxdQ+w==} + sass-embedded@1.105.1: + resolution: {integrity: sha512-9tKxXG4rq+oGTZUlQehWZwHxBTL80EiB8IWk9AEqz8uOqg910t88JIYbN92eMCxokQ5vrYSQxg15bh0o/3hh3w==} engines: {node: '>=20.19.0'} hasBin: true - sass@1.105.0: - resolution: {integrity: sha512-4EjSuRTyG8M4yRXXY1PT9OUV5zz23xBxJkbIAprohKHqf6HOU4MCpE3U3vAC+iO1gxobBjESNbcj8rYAQZWnyA==} + sass@1.105.1: + resolution: {integrity: sha512-1CnAWPyrrnVQzXGB/DUep1vYCKCm1CIBr+YxjjiW6wk9qtzemnGP4qicH56T2F9S4Hjtv/Ne3jDz8JqfdDU4oA==} engines: {node: '>=20.19.0'} hasBin: true @@ -5235,11 +4415,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -5275,20 +4450,12 @@ packages: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - shebang-regex@1.0.0: resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} engines: {node: '>=0.10.0'} - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shell-quote@1.9.0: - resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} + shell-quote@1.12.0: + resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==} engines: {node: '>= 0.4'} side-channel-list@1.0.1: @@ -5333,8 +4500,8 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} source-map-support@0.5.21: @@ -5353,11 +4520,8 @@ packages: spdx-expression-parse@3.0.1: resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - spdx-license-ids@3.0.22: - resolution: {integrity: sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + spdx-license-ids@3.0.24: + resolution: {integrity: sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==} stable@0.1.8: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} @@ -5370,15 +4534,15 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@4.2.0: - resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - storybook@10.6.0: - resolution: {integrity: sha512-H48X6AURAFTpfN2zEux6vNesee5Oq8Gxj1IanYmblPZ218sfKSeyyerF1NC7GD7BrruoC8L4/cU0zII4y+d6zw==} + storybook@10.6.1: + resolution: {integrity: sha512-NeEmukMWPA5OqHizUkQtxCIhGt7MUZLPY4FFgpC0LNiipjrac/oKpH0TxI+9Zc+vI5+IoN+NiskOmBP7gtYfmA==} hasBin: true peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5395,10 +4559,6 @@ packages: stream@0.0.3: resolution: {integrity: sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==} - string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} @@ -5410,31 +4570,20 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string-width@8.2.2: - resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + string-width@8.3.0: + resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==} engines: {node: '>=20'} - string.prototype.includes@2.0.1: - resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} - engines: {node: '>= 0.4'} - - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} - string.prototype.padend@3.1.6: resolution: {integrity: sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==} engines: {node: '>= 0.4'} - string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} - - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} engines: {node: '>= 0.4'} - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} engines: {node: '>= 0.4'} string.prototype.trimstart@1.0.8: @@ -5471,10 +4620,6 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - stubborn-fs@1.2.5: resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} @@ -5526,8 +4671,8 @@ packages: peerDependencies: stylelint: ^16.8.2 || ^17.0.0 - stylelint@17.15.0: - resolution: {integrity: sha512-mWIkesYQvQjf4Kvdeu9ns0IL8/K/wtjaGxPqsPd6DlLZvaQxGysJsocxUDrBcyHQ5VleAk4uSMat4yMrVED7PA==} + stylelint@17.16.0: + resolution: {integrity: sha512-ejlWnbJ13x1GU32+zZXa6sYQJsKKkwSdv2B4rAdO3OWdEmvhSJKcXi5pOphQYSzyaj1kXpuGJr7pcMBNfDIuPw==} engines: {node: '>=20.19.0'} hasBin: true @@ -5539,16 +4684,12 @@ packages: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - supports-hyperlinks@4.5.0: - resolution: {integrity: sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==} + supports-hyperlinks@4.6.0: + resolution: {integrity: sha512-FL/q/YePS4tFFTNiBOR+zPk2ZrKheqNtmTJ/9OUZ4e1wKHJofDbGQSmSsxS0XNnabSkqexryCaXGvEKz1vf9UQ==} engines: {node: '>=20'} supports-preserve-symlinks-flag@1.0.0: @@ -5563,8 +4704,8 @@ packages: svg-tags@1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} - svgo@2.8.3: - resolution: {integrity: sha512-5EZD0pafXX6PphdwOGCiVLDSaV1xyuQao2blHajHLsPxr07q4mmEjdtXEWgG07ae2mIz8Ex2CDXNCTiXhy3Khw==} + svgo@2.8.4: + resolution: {integrity: sha512-2GJ4h3rl13qYTdwllaK6QlL8tG+UrM8626V2Ylcd/yBUv2Y/EwLsYisVV6UDCRmlk+C74G8nMpxJCk0RWPdDCw==} engines: {node: '>=10.13.0'} hasBin: true @@ -5605,8 +4746,8 @@ packages: tiny-readdir@2.7.4: resolution: {integrity: sha512-721U+zsYwDirjr8IM6jqpesD/McpZooeFi3Zc6mcjy1pse2C+v19eHPFRqz4chGXZFw7C3KITDjAtHETc2wj7Q==} - tinybench@6.2.0: - resolution: {integrity: sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==} + tinybench@6.2.1: + resolution: {integrity: sha512-IAbQaPJIIdhVxi0mqy9lez1wEwje0UQf9F1vwtbelEG5Nbgn0nPOxCvU+mNeNS9gyJTAhrHp74CbUaSynQpZcQ==} engines: {node: '>=20.0.0'} tinycolor2@1.6.0: @@ -5624,19 +4765,19 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} - tinyrainbow@3.1.1: - resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + tinyrainbow@3.2.0: + resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} engines: {node: '>=14.0.0'} - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} engines: {node: '>=14.0.0'} - tldts-core@7.4.10: - resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==} + tldts-core@7.4.16: + resolution: {integrity: sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==} - tldts@7.4.10: - resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} + tldts@7.4.16: + resolution: {integrity: sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==} hasBin: true to-regex-range@5.0.1: @@ -5662,9 +4803,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@6.0.0: - resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} - engines: {node: '>=20'} + tr46@7.0.0: + resolution: {integrity: sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==} + engines: {node: ^22.14.0 || >=24.0.0} tree-dump@1.1.0: resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} @@ -5684,9 +4825,6 @@ packages: resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} engines: {node: '>=6.10'} - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -5697,14 +4835,6 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - type-is@2.1.0: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} @@ -5717,19 +4847,14 @@ packages: resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} engines: {node: '>= 0.4'} - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + typed-array-byte-offset@1.0.5: + resolution: {integrity: sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==} engines: {node: '>= 0.4'} - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} - typescript@5.8.2: - resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} - engines: {node: '>=14.17'} - hasBin: true - typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -5756,20 +4881,16 @@ packages: resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} engines: {node: '>=22.19.0'} - unicorn-magic@0.4.0: - resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + unicorn-magic@0.4.1: + resolution: {integrity: sha512-lzlXPoVB0Uy/FvTaUgX39RbixYiSKoc3JSoSf01+0c2B6FR3qdYIPN7Qjo/AV7n6sqLCk0509cT3LRj1oSZ1+A==} engines: {node: '>=20'} - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin-dts@1.1.1: - resolution: {integrity: sha512-Z2VcabC0hjYuzF62oWmZXVoxagkM8bUWgqUy+dotoJTPnooR5mNaxbpv63JCmcaxdGCW2Bb75omK6FOryOYCRg==} + unplugin-dts@1.1.2: + resolution: {integrity: sha512-RDqy90XUmFj6rz4U5YoTMdYdaCJe2ZnQZaWIjkWS4952Gv0zBTQyqWtlGgVMPYHa982dURvyoc+sUOFIcDFv+w==} peerDependencies: '@microsoft/api-extractor': '>=7' '@rspack/core': ^1 @@ -5802,27 +4923,18 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.3.3: resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - url@0.11.4: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + use-sync-external-store@1.7.0: + resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5853,8 +4965,8 @@ packages: resolution: {integrity: sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==} engines: {node: '>= 0.10'} - vite-plugin-dts@5.1.1: - resolution: {integrity: sha512-U5dyX3DPFvHB+gaJIULn90kKGsXNYllyJ3vn491Y+LGu2nL5Fb8z7YgurN/Pi6ZFlnGljh2wSM0ZbFogSh4yAg==} + vite-plugin-dts@5.1.2: + resolution: {integrity: sha512-1bXUz836sWq6ewEMQre3ZtNFVfZk/92DppgCCKP8tWx/NWwV2wHuXlRHb2TztHjbikPxvyAGLGu7hVh94Plwww==} peerDependencies: '@microsoft/api-extractor': '>=7' rollup: '>=3' @@ -5867,8 +4979,8 @@ packages: vite: optional: true - vite@8.3.1: - resolution: {integrity: sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==} + vite@8.3.3: + resolution: {integrity: sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5910,20 +5022,20 @@ packages: yaml: optional: true - vitest@5.0.2: - resolution: {integrity: sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==} + vitest@5.0.3: + resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 5.0.2 - '@vitest/browser-preview': 5.0.2 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-preview': 5.0.3 '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 - '@vitest/coverage-istanbul': 5.0.2 - '@vitest/coverage-v8': 5.0.2 - '@vitest/ui': 5.0.2 + '@vitest/coverage-istanbul': 5.0.3 + '@vitest/coverage-v8': 5.0.3 + '@vitest/ui': 5.0.3 happy-dom: '*' jsdom: '*' vite: ^6.4.0 || ^7.0.0 || ^8.0.0 @@ -5951,15 +5063,15 @@ packages: jsdom: optional: true - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vscode-uri@3.2.0: + resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} w3c-xmlserializer@6.0.0: resolution: {integrity: sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - wait-on@9.1.0: - resolution: {integrity: sha512-PymrLXHLBM1Ju/Xspb2ADUhbPSMvbnuNvy/mN2hWtpbJ3da0h3Ky1LqwKPG5QSVR57liyO0iUpfipYl/s5qNvA==} + wait-on@9.5.1: + resolution: {integrity: sha512-bOtj1wRY6u+GPnNskZLcgZNHOaL5NC4+5hRCbEpVoVX6MbMrCLCqIBM2bjsAfrgst3ZC0siIo7vLb4LuYFfIow==} engines: {node: '>=20.0.0'} hasBin: true @@ -5980,12 +5092,8 @@ packages: resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} engines: {node: '>=20'} - whatwg-url@16.0.1: - resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - whatwg-url@17.1.2: - resolution: {integrity: sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ==} + whatwg-url@17.2.0: + resolution: {integrity: sha512-IhLIHNcBrzIixC5XXsbr4DxdjajyA5vQ5RwGZMtrUp+GbtE5Nrsge9QKau7jXrC0aTXLYGV0xiW7Tb4YS2aomw==} engines: {node: ^22.14.0 || >=24.0.0} whatwg-url@5.0.0: @@ -6003,21 +5111,16 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.22: - resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + which-typed-array@1.1.24: + resolution: {integrity: sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==} engines: {node: '>= 0.4'} which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - why-is-node-running@3.2.2: - resolution: {integrity: sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==} + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} engines: {node: '>=20.11'} hasBin: true @@ -6029,10 +5132,6 @@ packages: resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} engines: {node: '>= 12.0.0'} - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - workerpool@10.0.3: resolution: {integrity: sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==} @@ -6051,18 +5150,6 @@ packages: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} - ws@8.21.2: - resolution: {integrity: sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.22.0: resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} engines: {node: '>=10.0.0'} @@ -6100,9 +5187,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -6111,8 +5195,8 @@ packages: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} engines: {node: '>=12'} yargs@18.0.0: @@ -6123,42 +5207,29 @@ packages: resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} engines: {node: '>=12'} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - yocto-queue@1.2.2: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} - zod-validation-error@4.0.2: - resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - snapshots: '@adobe/css-tools@4.5.0': {} - '@ark/schema@0.56.0': + '@ark/schema@0.56.6': dependencies: - '@ark/util': 0.56.0 + '@ark/util': 0.56.6 - '@ark/util@0.56.0': {} + '@ark/util@0.56.6': {} - '@asamuzakjp/css-color@7.1.2': + '@asamuzakjp/css-color@7.1.3': dependencies: - '@csstools/css-calc': 3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) - '@csstools/css-color-parser': 4.2.4(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) - '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-color-parser': 4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) '@csstools/css-tokenizer': 4.0.2 lru-cache: 11.5.3 - '@asamuzakjp/dom-selector@9.2.2': + '@asamuzakjp/dom-selector@9.2.4': dependencies: bidi-js: 1.1.0 css-tree: 3.2.1 @@ -6171,11 +5242,6 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/code-frame@8.0.0': - dependencies: - '@babel/helper-validator-identifier': 8.0.2 - js-tokens: 10.0.0 - '@babel/code-frame@8.0.6': dependencies: '@babel/helper-validator-identifier': 8.0.6 @@ -6188,14 +5254,14 @@ snapshots: '@babel/core@7.29.7(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@10.2.2) - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@10.2.2) + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@10.2.2) @@ -6208,14 +5274,14 @@ snapshots: '@babel/core@7.29.7(supports-color@5.5.0)': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@5.5.0))(supports-color@5.5.0) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@5.5.0) - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@5.5.0) + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@5.5.0) @@ -6237,38 +5303,21 @@ snapshots: '@babel/types': 8.0.6 '@types/gensync': 1.0.5 convert-source-map: 2.0.0 - empathic: 2.0.1 + empathic: 2.1.0 gensync: 1.0.0-beta.2 import-meta-resolve: 4.2.0 json5: 2.2.3 - obug: 2.1.4 + obug: 2.2.1 verkit: 0.3.2 - '@babel/generator@7.29.7': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - '@babel/generator@7.29.8': dependencies: - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/generator@8.0.0': - dependencies: - '@babel/parser': 8.0.0 - '@babel/types': 8.0.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@types/jsesc': 2.5.1 - jsesc: 3.1.0 - '@babel/generator@8.0.6': dependencies: '@babel/parser': 8.0.6 @@ -6280,13 +5329,13 @@ snapshots: '@babel/helper-annotate-as-pure@8.0.0': dependencies: - '@babel/types': 8.0.0 + '@babel/types': 8.0.6 '@babel/helper-compilation-targets@7.29.7': dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.7 + browserslist: 4.29.3 lru-cache: 5.1.1 semver: 6.3.1 @@ -6294,41 +5343,39 @@ snapshots: dependencies: '@babel/compat-data': 8.0.5 '@babel/helper-validator-option': 8.0.0 - browserslist: 4.29.2 + browserslist: 4.29.3 flru: 1.0.2 verkit: 0.3.2 '@babel/helper-globals@7.29.7': {} - '@babel/helper-globals@8.0.0': {} - '@babel/helper-globals@8.0.6': {} '@babel/helper-module-imports@7.29.7(supports-color@10.2.2)': dependencies: - '@babel/traverse': 7.29.7(supports-color@10.2.2) - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@10.2.2) + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.29.7(supports-color@5.5.0)': dependencies: - '@babel/traverse': 7.29.7(supports-color@5.5.0) - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@5.5.0) + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@8.0.0': dependencies: - '@babel/traverse': 8.0.0 - '@babel/types': 8.0.0 + '@babel/traverse': 8.0.6 + '@babel/types': 8.0.6 '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@10.2.2) + '@babel/traverse': 7.29.8(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -6337,7 +5384,7 @@ snapshots: '@babel/core': 7.29.7(supports-color@5.5.0) '@babel/helper-module-imports': 7.29.7(supports-color@5.5.0) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@5.5.0) + '@babel/traverse': 7.29.8(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -6347,14 +5394,10 @@ snapshots: '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-string-parser@8.0.0': {} - '@babel/helper-string-parser@8.0.6': {} '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-identifier@8.0.2': {} - '@babel/helper-validator-identifier@8.0.6': {} '@babel/helper-validator-option@7.29.7': {} @@ -6364,25 +5407,17 @@ snapshots: '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/helpers@8.0.5': dependencies: '@babel/template': 8.0.0 '@babel/types': 8.0.6 - '@babel/parser@7.29.7': - dependencies: - '@babel/types': 7.29.7 - - '@babel/parser@7.29.8': + '@babel/parser@7.29.9': dependencies: '@babel/types': 7.29.8 - '@babel/parser@8.0.0': - dependencies: - '@babel/types': 8.0.0 - '@babel/parser@8.0.6': dependencies: '@babel/types': 8.0.6 @@ -6409,7 +5444,7 @@ snapshots: '@babel/helper-module-imports': 8.0.0 '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.6) '@babel/plugin-syntax-jsx': 8.0.1(@babel/core@8.0.6) - '@babel/types': 8.0.0 + '@babel/types': 8.0.6 '@babel/plugin-transform-react-pure-annotations@8.0.1(@babel/core@8.0.6)': dependencies: @@ -6429,52 +5464,28 @@ snapshots: '@babel/runtime-corejs3@7.29.7': dependencies: - core-js-pure: 3.48.0 + core-js-pure: 3.50.0 '@babel/runtime@7.29.7': {} '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@babel/template@8.0.0': dependencies: - '@babel/code-frame': 8.0.0 - '@babel/parser': 8.0.0 - '@babel/types': 8.0.0 - - '@babel/traverse@7.29.7(supports-color@10.2.2)': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - - '@babel/traverse@7.29.7(supports-color@5.5.0)': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - debug: 4.4.3(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color + '@babel/code-frame': 8.0.6 + '@babel/parser': 8.0.6 + '@babel/types': 8.0.6 '@babel/traverse@7.29.8(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 '@babel/types': 7.29.8 debug: 4.4.3(supports-color@10.2.2) @@ -6486,23 +5497,13 @@ snapshots: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 '@babel/types': 7.29.8 debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@babel/traverse@8.0.0': - dependencies: - '@babel/code-frame': 8.0.0 - '@babel/generator': 8.0.0 - '@babel/helper-globals': 8.0.0 - '@babel/parser': 8.0.0 - '@babel/template': 8.0.0 - '@babel/types': 8.0.0 - obug: 2.1.4 - '@babel/traverse@8.0.6': dependencies: '@babel/code-frame': 8.0.6 @@ -6511,23 +5512,13 @@ snapshots: '@babel/parser': 8.0.6 '@babel/template': 8.0.0 '@babel/types': 8.0.6 - obug: 2.1.4 - - '@babel/types@7.29.7': - dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 + obug: 2.2.1 '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/types@8.0.0': - dependencies: - '@babel/helper-string-parser': 8.0.0 - '@babel/helper-validator-identifier': 8.0.2 - '@babel/types@8.0.6': dependencies: '@babel/helper-string-parser': 8.0.6 @@ -6537,11 +5528,11 @@ snapshots: '@blazediff/core@1.10.0': {} - '@bramus/specificity@2.4.2': + '@bramus/specificity@2.4.3': dependencies: css-tree: 3.2.1 - '@bufbuild/protobuf@2.12.1': {} + '@bufbuild/protobuf@2.16.0': {} '@bundled-es-modules/deepmerge@4.3.2': dependencies: @@ -6557,17 +5548,15 @@ snapshots: string_decoder: 1.3.0 url: 0.11.4 - '@bundled-es-modules/memfs@4.17.0(tslib@2.8.1)': + '@bundled-es-modules/memfs@4.17.0': dependencies: assert: 2.1.0 buffer: 6.0.3 events: 3.3.0 - memfs: 4.64.0(tslib@2.8.1) + memfs: 4.80.0 path: 0.12.7 stream: 0.0.3 util: 0.12.5 - transitivePeerDependencies: - - tslib '@bundled-es-modules/postcss-calc-ast-parser@0.1.6': dependencies: @@ -6587,55 +5576,46 @@ snapshots: '@colors/colors@1.6.0': {} + '@colors/colors@1.6.1': {} + '@csstools/color-helpers@6.1.2': {} - '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-calc@3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': dependencies: - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - - '@csstools/css-calc@3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': - dependencies: - '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) '@csstools/css-tokenizer': 4.0.2 - '@csstools/css-color-parser@4.2.4(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + '@csstools/css-color-parser@4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': dependencies: '@csstools/color-helpers': 6.1.2 - '@csstools/css-calc': 3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) - '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) '@csstools/css-tokenizer': 4.0.2 - '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': - dependencies: - '@csstools/css-tokenizer': 4.0.0 - - '@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2)': + '@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2)': dependencies: '@csstools/css-tokenizer': 4.0.2 - '@csstools/css-syntax-patches-for-csstree@1.1.14(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.15(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 - '@csstools/css-tokenizer@4.0.0': {} - '@csstools/css-tokenizer@4.0.2': {} - '@csstools/media-query-list-parser@5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/media-query-list-parser@5.0.2(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': dependencies: - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 - '@csstools/selector-resolve-nested@4.0.1(postcss-selector-parser@7.1.6)': + '@csstools/selector-resolve-nested@4.0.2(postcss-selector-parser@7.1.6)': dependencies: postcss-selector-parser: 7.1.6 - '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.6)': + '@csstools/selector-specificity@6.0.1(postcss-selector-parser@7.1.6)': dependencies: postcss-selector-parser: 7.1.6 - '@dabh/diagnostics@2.0.8': + '@dabh/diagnostics@2.0.9': dependencies: '@so-ric/colorspace': 1.1.6 enabled: 2.0.0 @@ -6653,12 +5633,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/core@2.0.0-alpha.3': - dependencies: - '@emnapi/wasi-threads': 2.0.1 - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.0': dependencies: tslib: 2.8.1 @@ -6669,11 +5643,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@2.0.0-alpha.3': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -6684,11 +5653,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@2.0.1': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.28.2': optional: true @@ -6767,53 +5731,7 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(supports-color@10.2.2))': - dependencies: - eslint: 9.39.2(supports-color@10.2.2) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.21.2(supports-color@10.2.2)': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@10.2.2) - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.5(supports-color@10.2.2)': - dependencies: - ajv: 6.15.0 - debug: 4.4.3(supports-color@10.2.2) - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.3.0 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.2': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 - - '@exodus/bytes@1.15.1': {} + '@exodus/bytes@1.16.0': {} '@hapi/address@5.1.1': dependencies: @@ -6831,22 +5749,6 @@ snapshots: dependencies: '@hapi/hoek': 11.0.7 - '@humanfs/core@0.19.2': - dependencies: - '@humanfs/types': 0.15.0 - - '@humanfs/node@0.16.8': - dependencies: - '@humanfs/core': 0.19.2 - '@humanfs/types': 0.15.0 - '@humanwhocodes/retry': 0.4.3 - - '@humanfs/types@0.15.0': {} - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - '@internationalized/date@3.12.4': dependencies: '@swc/helpers': 0.5.23 @@ -6859,17 +5761,17 @@ snapshots: dependencies: '@swc/helpers': 0.5.23 - '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@6.0.3) - vite: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) + vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) optionalDependencies: typescript: 6.0.3 '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/gen-mapping@0.4.0-beta.0': @@ -6884,14 +5786,12 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: @@ -6917,59 +5817,59 @@ snapshots: dependencies: tslib: 2.8.1 - '@jsonjoy.com/fs-core@4.64.0(tslib@2.8.1)': + '@jsonjoy.com/fs-core@4.80.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-node-builtins': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.64.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) thingies: 2.6.1(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-fsa@4.64.0(tslib@2.8.1)': + '@jsonjoy.com/fs-fsa@4.80.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-core': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.64.0(tslib@2.8.1) + '@jsonjoy.com/fs-core': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) thingies: 2.6.1(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-node-builtins@4.64.0(tslib@2.8.1)': + '@jsonjoy.com/fs-node-builtins@4.80.0(tslib@2.8.1)': dependencies: tslib: 2.8.1 - '@jsonjoy.com/fs-node-to-fsa@4.64.0(tslib@2.8.1)': + '@jsonjoy.com/fs-node-to-fsa@4.80.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-fsa': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.64.0(tslib@2.8.1) + '@jsonjoy.com/fs-fsa': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-node-utils@4.64.0(tslib@2.8.1)': + '@jsonjoy.com/fs-node-utils@4.80.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-node-builtins': 4.64.0(tslib@2.8.1) - glob-to-regex.js: 1.2.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-node@4.64.0(tslib@2.8.1)': + '@jsonjoy.com/fs-node@4.80.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-core': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-print': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-snapshot': 4.64.0(tslib@2.8.1) - glob-to-regex.js: 1.2.0(tslib@2.8.1) + '@jsonjoy.com/fs-core': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.80.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) thingies: 2.6.1(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-print@4.64.0(tslib@2.8.1)': + '@jsonjoy.com/fs-print@4.80.0(tslib@2.8.1)': dependencies: - '@jsonjoy.com/fs-node-utils': 4.64.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 - '@jsonjoy.com/fs-snapshot@4.64.0(tslib@2.8.1)': + '@jsonjoy.com/fs-snapshot@4.80.0(tslib@2.8.1)': dependencies: '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.64.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) '@jsonjoy.com/json-pack': 17.67.0(tslib@2.8.1) '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) tslib: 2.8.1 @@ -7035,65 +5935,18 @@ snapshots: '@types/react': 19.3.0 react: 19.3.0 - '@microsoft/api-extractor-model@7.32.2(@types/node@26.6.3)': - dependencies: - '@microsoft/tsdoc': 0.16.0 - '@microsoft/tsdoc-config': 0.18.1 - '@rushstack/node-core-library': 5.19.1(@types/node@26.6.3) - transitivePeerDependencies: - - '@types/node' - optional: true - - '@microsoft/api-extractor@7.56.2(@types/node@26.6.3)': - dependencies: - '@microsoft/api-extractor-model': 7.32.2(@types/node@26.6.3) - '@microsoft/tsdoc': 0.16.0 - '@microsoft/tsdoc-config': 0.18.1 - '@rushstack/node-core-library': 5.19.1(@types/node@26.6.3) - '@rushstack/rig-package': 0.6.0 - '@rushstack/terminal': 0.21.0(@types/node@26.6.3) - '@rushstack/ts-command-line': 5.2.0(@types/node@26.6.3) - diff: 8.0.4 - lodash: 4.18.1 - minimatch: 10.2.5 - resolve: 1.22.12 - semver: 7.5.4 - source-map: 0.6.1 - typescript: 5.8.2 - transitivePeerDependencies: - - '@types/node' - optional: true - - '@microsoft/tsdoc-config@0.18.1': - dependencies: - '@microsoft/tsdoc': 0.16.0 - ajv: 8.18.0 - jju: 1.4.0 - resolve: 1.22.12 - optional: true - - '@microsoft/tsdoc@0.16.0': - optional: true - - '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': + '@napi-rs/wasm-runtime@1.2.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': dependencies: '@emnapi/core': 1.11.0 '@emnapi/runtime': 1.11.0 - '@tybys/wasm-util': 0.10.3 + '@tybys/wasm-util': 0.10.4 optional: true - '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@napi-rs/wasm-runtime@1.2.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 '@emnapi/runtime': 1.9.2 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': - dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 - '@tybys/wasm-util': 0.10.3 + '@tybys/wasm-util': 0.10.4 optional: true '@nodelib/fs.scandir@2.1.5': @@ -7106,7 +5959,7 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + fastq: 1.20.3 '@one-ini/wasm@0.2.1': {} @@ -7162,7 +6015,7 @@ snapshots: dependencies: '@emnapi/core': 1.9.2 '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@napi-rs/wasm-runtime': 1.2.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true '@oxc-parser/binding-win32-arm64-msvc@0.127.0': @@ -7176,10 +6029,7 @@ snapshots: '@oxc-project/types@0.127.0': {} - '@oxc-project/types@0.142.0': - optional: true - - '@oxc-project/types@0.151.0': {} + '@oxc-project/types@0.152.0': {} '@oxc-resolver/binding-android-arm-eabi@11.21.2': optional: true @@ -7233,7 +6083,7 @@ snapshots: dependencies: '@emnapi/core': 1.11.0 '@emnapi/runtime': 1.11.0 - '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) + '@napi-rs/wasm-runtime': 1.2.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) optional: true '@oxc-resolver/binding-win32-arm64-msvc@11.21.2': @@ -7283,7 +6133,7 @@ snapshots: detect-libc: 2.1.2 is-glob: 4.0.3 node-addon-api: 7.1.1 - picomatch: 4.0.5 + picomatch: 4.0.7 optionalDependencies: '@parcel/watcher-android-arm64': 2.6.0 '@parcel/watcher-darwin-arm64': 2.6.0 @@ -7370,230 +6220,58 @@ snapshots: '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 '@resvg/resvg-js-win32-x64-msvc': 2.6.2 - '@rolldown/binding-android-arm-eabi@1.2.11': + '@rolldown/binding-android-arm-eabi@1.2.12': optional: true - '@rolldown/binding-android-arm64@1.2.1': + '@rolldown/binding-android-arm64@1.2.12': optional: true - '@rolldown/binding-android-arm64@1.2.11': + '@rolldown/binding-darwin-arm64@1.2.12': optional: true - '@rolldown/binding-darwin-arm64@1.2.1': + '@rolldown/binding-darwin-x64@1.2.12': optional: true - '@rolldown/binding-darwin-arm64@1.2.11': + '@rolldown/binding-freebsd-x64@1.2.12': optional: true - '@rolldown/binding-darwin-x64@1.2.1': + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': optional: true - '@rolldown/binding-darwin-x64@1.2.11': + '@rolldown/binding-linux-arm64-gnu@1.2.12': optional: true - '@rolldown/binding-freebsd-x64@1.2.1': + '@rolldown/binding-linux-arm64-musl@1.2.12': optional: true - '@rolldown/binding-freebsd-x64@1.2.11': + '@rolldown/binding-linux-ppc64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.1': + '@rolldown/binding-linux-s390x-gnu@1.2.12': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + '@rolldown/binding-linux-x64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.1': + '@rolldown/binding-linux-x64-musl@1.2.12': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.11': + '@rolldown/binding-openharmony-arm64@1.2.12': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.1': + '@rolldown/binding-win32-arm64-msvc@1.2.12': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.11': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.2.1': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.2.11': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.2.1': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.2.11': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.2.1': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.2.11': - optional: true - - '@rolldown/binding-linux-x64-musl@1.2.1': - optional: true - - '@rolldown/binding-linux-x64-musl@1.2.11': - optional: true - - '@rolldown/binding-openharmony-arm64@1.2.1': - optional: true - - '@rolldown/binding-openharmony-arm64@1.2.11': - optional: true - - '@rolldown/binding-wasm32-wasi@1.2.1': - dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 - '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.2.1': - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.2.11': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.2.1': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.2.11': + '@rolldown/binding-win32-x64-msvc@1.2.12': optional: true '@rolldown/pluginutils@1.0.1': {} - '@rollup/pluginutils@5.4.0(rollup@4.61.1)': + '@rollup/pluginutils@5.4.0': dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 - picomatch: 4.0.5 - optionalDependencies: - rollup: 4.61.1 - - '@rollup/rollup-android-arm-eabi@4.61.1': - optional: true - - '@rollup/rollup-android-arm64@4.61.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.61.1': - optional: true - - '@rollup/rollup-darwin-x64@4.61.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.61.1': - optional: true - - '@rollup/rollup-freebsd-x64@4.61.1': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.61.1': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.61.1': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.61.1': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.61.1': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.61.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.61.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.61.1': - optional: true - - '@rollup/rollup-openbsd-x64@4.61.1': - optional: true - - '@rollup/rollup-openharmony-arm64@4.61.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.61.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.61.1': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.61.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.61.1': - optional: true - - '@rtsao/scc@1.1.0': {} - - '@rushstack/node-core-library@5.19.1(@types/node@26.6.3)': - dependencies: - ajv: 8.20.0 - ajv-draft-04: 1.0.0(ajv@8.20.0) - ajv-formats: 3.0.1(ajv@8.20.0) - fs-extra: 11.3.6 - import-lazy: 4.0.0 - jju: 1.4.0 - resolve: 1.22.12 - semver: 7.5.4 - optionalDependencies: - '@types/node': 26.6.3 - optional: true - - '@rushstack/problem-matcher@0.1.1(@types/node@26.6.3)': - optionalDependencies: - '@types/node': 26.6.3 - optional: true - - '@rushstack/rig-package@0.6.0': - dependencies: - resolve: 1.22.12 - strip-json-comments: 3.1.1 - optional: true - - '@rushstack/terminal@0.21.0(@types/node@26.6.3)': - dependencies: - '@rushstack/node-core-library': 5.19.1(@types/node@26.6.3) - '@rushstack/problem-matcher': 0.1.1(@types/node@26.6.3) - supports-color: 8.1.1 - optionalDependencies: - '@types/node': 26.6.3 - optional: true - - '@rushstack/ts-command-line@5.2.0(@types/node@26.6.3)': - dependencies: - '@rushstack/terminal': 0.21.0(@types/node@26.6.3) - '@types/argparse': 1.0.38 - argparse: 1.0.10 - string-argv: 0.3.2 - transitivePeerDependencies: - - '@types/node' - optional: true + picomatch: 4.0.7 '@sindresorhus/merge-streams@4.0.0': {} @@ -7604,14 +6282,14 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))': + '@storybook/addon-docs@10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.3.0)(react@19.3.0) '@storybook/icons': 2.1.0(react@19.3.0) - '@storybook/react-dom-shim': 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0)) + '@storybook/react-dom-shim': 10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0)) react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - storybook: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + storybook: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) ts-dedent: 2.3.0 unplugin: 2.3.11 optionalDependencies: @@ -7619,29 +6297,28 @@ snapshots: transitivePeerDependencies: - '@types/react-dom' - '@storybook/addon-themes@10.6.0(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))': + '@storybook/addon-themes@10.6.1(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))': dependencies: - storybook: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + storybook: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) ts-dedent: 2.3.0 - '@storybook/addon-vitest@10.6.0(@vitest/browser-playwright@5.0.2)(@vitest/browser@5.0.2)(@vitest/runner@4.1.10)(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(vitest@5.0.2)': + '@storybook/addon-vitest@10.6.1(@vitest/browser-playwright@5.0.3)(@vitest/browser@5.0.3)(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(vitest@5.0.3)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.1.0(react@19.3.0) - storybook: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + storybook: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) optionalDependencies: - '@vitest/browser': 5.0.2(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))(vitest@5.0.2) - '@vitest/browser-playwright': 5.0.2(playwright@1.62.1)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))(vitest@5.0.2) - '@vitest/runner': 4.1.10 - vitest: 5.0.2(@types/node@26.6.3)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1(canvas@3.2.3))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + '@vitest/browser': 5.0.3(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))(vitest@5.0.3) + '@vitest/browser-playwright': 5.0.3(playwright@1.62.1)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))(vitest@5.0.3) + vitest: 5.0.3(@types/node@26.6.4)(@vitest/browser-playwright@5.0.3)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(jsdom@30.1.2(canvas@3.2.3))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) transitivePeerDependencies: - react - '@storybook/builder-vite@10.6.0(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))': + '@storybook/builder-vite@10.6.1(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))': dependencies: - storybook: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + storybook: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) ts-dedent: 2.3.0 - vite: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) + vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) '@storybook/global@5.0.0': {} @@ -7649,30 +6326,30 @@ snapshots: dependencies: react: 19.3.0 - '@storybook/react-dom-shim@10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))': + '@storybook/react-dom-shim@10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))': dependencies: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - storybook: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + storybook: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) optionalDependencies: '@types/react': 19.3.0 '@types/react-dom': 19.3.0(@types/react@19.3.0) - '@storybook/react-vite@10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rollup@4.61.1)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))': + '@storybook/react-vite@10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) - '@rollup/pluginutils': 5.4.0(rollup@4.61.1) - '@storybook/builder-vite': 10.6.0(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) - '@storybook/react': 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@10.2.2)(typescript@6.0.3) - empathic: 2.0.1 - magic-string: 1.4.2 + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) + '@rollup/pluginutils': 5.4.0 + '@storybook/builder-vite': 10.6.1(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) + '@storybook/react': 10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@10.2.2)(typescript@6.0.3) + empathic: 2.1.0 + magic-string: 1.4.3 react: 19.3.0 - react-docgen: 8.0.3(supports-color@10.2.2) + react-docgen: 8.0.4(supports-color@10.2.2) react-dom: 19.3.0(react@19.3.0) resolve: 1.22.12 - storybook: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + storybook: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) tsconfig-paths: 4.2.0 - vite: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) + vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -7681,21 +6358,21 @@ snapshots: - rollup - supports-color - '@storybook/react-vite@10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(rollup@4.61.1)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@5.5.0)(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))': + '@storybook/react-vite@10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@5.5.0)(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) - '@rollup/pluginutils': 5.4.0(rollup@4.61.1) - '@storybook/builder-vite': 10.6.0(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) - '@storybook/react': 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@5.5.0)(typescript@6.0.3) - empathic: 2.0.1 - magic-string: 1.4.2 + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) + '@rollup/pluginutils': 5.4.0 + '@storybook/builder-vite': 10.6.1(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) + '@storybook/react': 10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@5.5.0)(typescript@6.0.3) + empathic: 2.1.0 + magic-string: 1.4.3 react: 19.3.0 - react-docgen: 8.0.3(supports-color@5.5.0) + react-docgen: 8.0.4(supports-color@5.5.0) react-dom: 19.3.0(react@19.3.0) resolve: 1.22.12 - storybook: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + storybook: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) tsconfig-paths: 4.2.0 - vite: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) + vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: @@ -7704,15 +6381,15 @@ snapshots: - rollup - supports-color - '@storybook/react@10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@10.2.2)(typescript@6.0.3)': + '@storybook/react@10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0)) + '@storybook/react-dom-shim': 10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0)) react: 19.3.0 - react-docgen: 8.0.3(supports-color@10.2.2) + react-docgen: 8.0.4(supports-color@10.2.2) react-docgen-typescript: 2.4.0(typescript@6.0.3) react-dom: 19.3.0(react@19.3.0) - storybook: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + storybook: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) optionalDependencies: '@types/react': 19.3.0 '@types/react-dom': 19.3.0(@types/react@19.3.0) @@ -7720,15 +6397,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react@10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@5.5.0)(typescript@6.0.3)': + '@storybook/react@10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(supports-color@5.5.0)(typescript@6.0.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0)) + '@storybook/react-dom-shim': 10.6.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0)) react: 19.3.0 - react-docgen: 8.0.3(supports-color@5.5.0) + react-docgen: 8.0.4(supports-color@5.5.0) react-docgen-typescript: 2.4.0(typescript@6.0.3) react-dom: 19.3.0(react@19.3.0) - storybook: 10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) + storybook: 10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0) optionalDependencies: '@types/react': 19.3.0 '@types/react-dom': 19.3.0(@types/react@19.3.0) @@ -7770,11 +6447,11 @@ snapshots: '@types/react': 19.3.0 '@types/react-dom': 19.3.0(@types/react@19.3.0) - '@testing-library/user-event@14.6.3(@testing-library/dom@10.4.2)': + '@testing-library/user-event@14.6.7(@testing-library/dom@10.4.2)': dependencies: '@testing-library/dom': 10.4.2 - '@tokens-studio/sd-transforms@2.0.3(style-dictionary@5.5.5(tslib@2.8.1))': + '@tokens-studio/sd-transforms@2.0.3(style-dictionary@5.5.5)': dependencies: '@bundled-es-modules/deepmerge': 4.3.2 '@bundled-es-modules/postcss-calc-ast-parser': 0.1.6 @@ -7782,30 +6459,27 @@ snapshots: colorjs.io: 0.5.2 expr-eval-fork: 3.0.3 is-mergeable-object: 1.1.1 - style-dictionary: 5.5.5(tslib@2.8.1) + style-dictionary: 5.5.5 '@tokens-studio/tokenscript-interpreter@0.26.0': dependencies: - arktype: 2.1.29 + arktype: 2.2.7 commander: 14.0.3 readline-sync: 1.4.10 yauzl: 3.4.0 '@tokens-studio/types@0.5.2': {} - '@tybys/wasm-util@0.10.3': + '@tybys/wasm-util@0.10.4': dependencies: tslib: 2.8.1 optional: true - '@types/argparse@1.0.38': - optional: true - '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/types': 7.29.8 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 @@ -7817,7 +6491,7 @@ snapshots: '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/types': 7.29.8 '@types/babel__traverse@7.28.0': @@ -7839,13 +6513,9 @@ snapshots: '@types/jsesc@2.5.1': {} - '@types/json-schema@7.0.15': {} - - '@types/json5@0.0.29': {} - '@types/mdx@2.0.14': {} - '@types/node@26.6.3': + '@types/node@26.6.4': dependencies: undici-types: 8.9.0 @@ -7861,37 +6531,37 @@ snapshots: '@types/triple-beam@1.3.5': {} - '@vitejs/plugin-react@6.1.1(babel-plugin-react-compiler@1.0.0)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))': + '@vitejs/plugin-react@6.1.2(babel-plugin-react-compiler@1.0.0)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) + vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) optionalDependencies: babel-plugin-react-compiler: 1.0.0 - '@vitest/browser-playwright@5.0.2(playwright@1.62.1)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))(vitest@5.0.2)': + '@vitest/browser-playwright@5.0.3(playwright@1.62.1)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))(vitest@5.0.3)': dependencies: - '@vitest/browser': 5.0.2(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))(vitest@5.0.2) - '@vitest/mocker': 5.0.2(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + '@vitest/browser': 5.0.3(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))(vitest@5.0.3) + '@vitest/mocker': 5.0.3(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) playwright: 1.62.1 - tinyrainbow: 3.1.1 - vitest: 5.0.2(@types/node@26.6.3)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1(canvas@3.2.3))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + tinyrainbow: 3.2.0 + vitest: 5.0.3(@types/node@26.6.4)(@vitest/browser-playwright@5.0.3)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(jsdom@30.1.2(canvas@3.2.3))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@5.0.2(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))(vitest@5.0.2)': + '@vitest/browser@5.0.3(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))(vitest@5.0.3)': dependencies: '@blazediff/core': 1.10.0 - '@vitest/mocker': 5.0.2(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) - '@vitest/ui': 5.0.2(vitest@5.0.2) - '@vitest/utils': 5.0.2 - magic-string: 1.4.2 + '@vitest/mocker': 5.0.3(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) + '@vitest/ui': 5.0.3(vitest@5.0.3) + '@vitest/utils': 5.0.3 + magic-string: 1.4.3 pngjs: 7.0.0 sirv: 3.0.2 - tinyrainbow: 3.1.1 - vitest: 5.0.2(@types/node@26.6.3)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1(canvas@3.2.3))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + tinyrainbow: 3.2.0 + vitest: 5.0.3(@types/node@26.6.4)(@vitest/browser-playwright@5.0.3)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(jsdom@30.1.2(canvas@3.2.3))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) ws: 8.22.0 transitivePeerDependencies: - bufferutil @@ -7899,19 +6569,19 @@ snapshots: - utf-8-validate - vite - '@vitest/coverage-v8@5.0.2(@vitest/browser@5.0.2)(vitest@5.0.2)': + '@vitest/coverage-v8@5.0.3(@vitest/browser@5.0.3)(vitest@5.0.3)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/istanbul-lib-coverage': 1.0.2 '@vitest/istanbul-lib-report': 1.0.2 - ast-v8-to-istanbul: 1.0.5 - magicast: 0.5.4 - obug: 2.1.4 - std-env: 4.2.0 - tinyrainbow: 3.1.1 - vitest: 5.0.2(@types/node@26.6.3)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1(canvas@3.2.3))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + ast-v8-to-istanbul: 1.0.7 + magicast: 0.5.5 + obug: 2.2.1 + std-env: 4.3.0 + tinyrainbow: 3.2.0 + vitest: 5.0.3(@types/node@26.6.4)(@vitest/browser-playwright@5.0.3)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(jsdom@30.1.2(canvas@3.2.3))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) optionalDependencies: - '@vitest/browser': 5.0.2(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))(vitest@5.0.2) + '@vitest/browser': 5.0.3(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))(vitest@5.0.3) '@vitest/expect@3.2.4': dependencies: @@ -7927,49 +6597,38 @@ snapshots: dependencies: '@vitest/istanbul-lib-coverage': 1.0.2 - '@vitest/mocker@5.0.2(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))': + '@vitest/mocker@5.0.3(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))': dependencies: '@jridgewell/trace-mapping': 0.3.31 - '@vitest/spy': 5.0.2 + '@vitest/spy': 5.0.3 estree-walker: 3.0.3 - magic-string: 1.4.2 + magic-string: 1.4.3 optionalDependencies: - vite: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) + vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.10': + '@vitest/pretty-format@5.0.3': dependencies: - tinyrainbow: 3.1.1 - optional: true - - '@vitest/pretty-format@5.0.2': - dependencies: - tinyrainbow: 3.1.1 - - '@vitest/runner@4.1.10': - dependencies: - '@vitest/utils': 4.1.10 - pathe: 2.0.3 - optional: true + tinyrainbow: 3.2.0 '@vitest/spy@3.2.4': dependencies: - tinyspy: 4.0.4 + tinyspy: 4.0.6 - '@vitest/spy@5.0.2': {} + '@vitest/spy@5.0.3': {} - '@vitest/ui@5.0.2(vitest@5.0.2)': + '@vitest/ui@5.0.3(vitest@5.0.3)': dependencies: - '@vitest/utils': 5.0.2 + '@vitest/utils': 5.0.3 fflate: 0.8.3 flatted: 3.4.4 pathe: 2.0.3 sirv: 3.0.2 - tinyrainbow: 3.1.1 - vitest: 5.0.2(@types/node@26.6.3)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1(canvas@3.2.3))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + tinyrainbow: 3.2.0 + vitest: 5.0.3(@types/node@26.6.4)(@vitest/browser-playwright@5.0.3)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(jsdom@30.1.2(canvas@3.2.3))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) '@vitest/utils@3.2.4': dependencies: @@ -7977,18 +6636,11 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@vitest/utils@4.1.10': + '@vitest/utils@5.0.3': dependencies: - '@vitest/pretty-format': 4.1.10 + '@vitest/pretty-format': 5.0.3 convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 - optional: true - - '@vitest/utils@5.0.2': - dependencies: - '@vitest/pretty-format': 5.0.2 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 + tinyrainbow: 3.2.0 '@volar/language-core@2.4.28': dependencies: @@ -8000,13 +6652,13 @@ snapshots: dependencies: '@volar/language-core': 2.4.28 path-browserify: 1.0.1 - vscode-uri: 3.1.0 + vscode-uri: 3.2.0 optionalDependencies: typescript: 6.0.3 '@webcontainer/env@1.1.1': {} - '@xmldom/xmldom@0.8.13': {} + '@xmldom/xmldom@0.8.15': {} '@zip.js/zip.js@2.8.34(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)': {} @@ -8015,13 +6667,9 @@ snapshots: accepts@2.0.0: dependencies: mime-types: 3.0.2 - negotiator: 1.0.0 + negotiator: 1.1.0 - acorn-jsx@5.3.2(acorn@8.18.0): - dependencies: - acorn: 8.18.0 - - acorn@8.18.0: {} + acorn@8.19.0: {} agent-base@6.0.2(supports-color@5.5.0): dependencies: @@ -8029,41 +6677,16 @@ snapshots: transitivePeerDependencies: - supports-color - ajv-draft-04@1.0.0(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 - optional: true - - ajv-formats@3.0.1(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 - optional: true - - ajv@6.15.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ajv@8.18.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - optional: true - ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.8 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 ansi-regex@5.0.1: {} - ansi-regex@6.2.2: {} + ansi-regex@6.4.0: {} ansi-styles@3.2.1: dependencies: @@ -8082,11 +6705,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - optional: true - argparse@2.0.1: {} aria-hidden@1.2.6: @@ -8099,79 +6717,27 @@ snapshots: aria-query@5.3.2: {} - arkregex@0.0.5: + arkregex@0.0.12: dependencies: - '@ark/util': 0.56.0 + '@ark/util': 0.56.6 - arktype@2.1.29: + arktype@2.2.7: dependencies: - '@ark/schema': 0.56.0 - '@ark/util': 0.56.0 - arkregex: 0.0.5 + '@ark/schema': 0.56.6 + '@ark/util': 0.56.6 + arkregex: 0.0.12 array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-includes@3.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array.prototype.findlast@1.2.5: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.findlastindex@1.2.6: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-shim-unscopables: 1.1.0 - - array.prototype.tosorted@1.1.4: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-errors: 1.3.0 - es-shim-unscopables: 1.1.0 - arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 @@ -8188,13 +6754,11 @@ snapshots: assertion-error@2.0.1: {} - ast-types-flow@0.0.8: {} - - ast-types@0.16.1: + ast-types@0.16.3: dependencies: tslib: 2.8.1 - ast-v8-to-istanbul@1.0.5: + ast-v8-to-istanbul@1.0.7: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -8208,24 +6772,22 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.6.1(postcss@8.5.28): + autoprefixer@10.6.1(postcss@8.5.29): dependencies: - browserslist: 4.29.2 - caniuse-lite: 1.0.30001813 + browserslist: 4.29.3 + caniuse-lite: 1.0.30001814 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.28 + postcss: 8.5.29 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.11.1: {} - - axios@1.19.0(debug@4.4.3(supports-color@5.5.0))(supports-color@5.5.0): + axios@1.20.0(debug@4.4.3(supports-color@5.5.0))(supports-color@5.5.0): dependencies: - follow-redirects: 1.16.0(debug@4.4.3(supports-color@5.5.0)) + follow-redirects: 1.16.1(debug@4.4.3(supports-color@5.5.0)) form-data: 4.0.6 https-proxy-agent: 5.0.1(supports-color@5.5.0) proxy-from-env: 2.1.0 @@ -8233,11 +6795,9 @@ snapshots: - debug - supports-color - axobject-query@4.1.0: {} - babel-plugin-react-compiler@1.0.0: dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 balanced-match@1.0.2: {} @@ -8245,9 +6805,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.11.26: {} - - baseline-browser-mapping@2.11.8: {} + baseline-browser-mapping@2.11.27: {} bidi-js@1.1.0: dependencies: @@ -8266,12 +6824,12 @@ snapshots: body-parser@2.3.0(supports-color@5.5.0): dependencies: bytes: 3.1.2 - content-type: 2.0.0 + content-type: 2.1.0 debug: 4.4.3(supports-color@5.5.0) http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.15.3 + qs: 6.16.0 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -8281,12 +6839,12 @@ snapshots: boolbase@2.0.0: {} - brace-expansion@1.1.18: + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.9: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -8294,21 +6852,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.7: + browserslist@4.29.3: dependencies: - baseline-browser-mapping: 2.11.8 - caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.399 - node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.7) - - browserslist@4.29.2: - dependencies: - baseline-browser-mapping: 2.11.26 - caniuse-lite: 1.0.30001813 - electron-to-chromium: 1.5.439 + baseline-browser-mapping: 2.11.27 + caniuse-lite: 1.0.30001814 + electron-to-chromium: 1.5.446 node-releases: 2.0.57 - update-browserslist-db: 1.3.3(browserslist@4.29.2) + update-browserslist-db: 1.3.3(browserslist@4.29.3) buffer-from@1.1.2: {} @@ -8322,7 +6872,7 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bundle-name@4.1.0: + bundle-name@4.1.1: dependencies: run-applescript: 7.1.0 @@ -8355,9 +6905,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001806: {} - - caniuse-lite@1.0.30001813: {} + caniuse-lite@1.0.30001814: {} canvas@3.2.3: dependencies: @@ -8372,7 +6920,7 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 - chai@6.2.2: {} + chai@6.3.0: {} chalk@2.4.2: dependencies: @@ -8380,11 +6928,6 @@ snapshots: escape-string-regexp: 1.0.5 supports-color: 5.5.0 - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chalk@5.6.2: {} change-case@5.4.4: {} @@ -8405,7 +6948,7 @@ snapshots: chokidar@5.0.0: dependencies: - readdirp: 5.0.0 + readdirp: 5.1.1 chownr@1.1.4: {} @@ -8453,17 +6996,17 @@ snapshots: color-convert@3.1.3: dependencies: - color-name: 2.1.0 + color-name: 2.1.1 color-name@1.1.3: {} color-name@1.1.4: {} - color-name@2.1.0: {} + color-name@2.1.1: {} color-string@2.1.4: dependencies: - color-name: 2.1.0 + color-name: 2.1.1 color-support@1.1.3: {} @@ -8515,25 +7058,25 @@ snapshots: dependencies: chalk: 5.6.2 rxjs: 7.8.2 - shell-quote: 1.9.0 + shell-quote: 1.12.0 supports-color: 10.2.2 tree-kill: 1.2.2 yargs: 18.0.0 confbox@0.1.8: {} - confbox@0.2.4: {} + confbox@0.3.1: {} config-chain@1.1.13: dependencies: ini: 1.3.8 proto-list: 1.2.4 - content-disposition@1.0.1: {} + content-disposition@1.1.0: {} content-type@1.0.5: {} - content-type@2.0.0: {} + content-type@2.1.0: {} convert-source-map@2.0.0: {} @@ -8541,7 +7084,7 @@ snapshots: cookie@0.7.2: {} - core-js-pure@3.48.0: {} + core-js-pure@3.50.0: {} core-util-is@1.0.3: {} @@ -8549,7 +7092,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.2 parse-json: 5.2.0 optionalDependencies: typescript: 6.0.3 @@ -8568,12 +7111,6 @@ snapshots: shebang-command: 1.2.0 which: 1.3.1 - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - css-functions-list@3.3.3: {} css-select@4.3.0: @@ -8602,12 +7139,12 @@ snapshots: css-tree@2.2.1: dependencies: mdn-data: 2.0.28 - source-map-js: 1.2.1 + source-map-js: 1.2.2 css-tree@3.2.1: dependencies: mdn-data: 2.27.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 css-what@6.2.2: {} @@ -8629,12 +7166,10 @@ snapshots: csstype@3.2.3: {} - damerau-levenshtein@1.0.8: {} - - data-urls@7.0.0: + data-urls@8.0.0: dependencies: whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 + whatwg-url: 17.2.0 transitivePeerDependencies: - '@noble/hashes' @@ -8664,12 +7199,6 @@ snapshots: optionalDependencies: supports-color: 5.5.0 - debug@3.2.7(supports-color@10.2.2): - dependencies: - ms: 2.1.3 - optionalDependencies: - supports-color: 10.2.2 - debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 @@ -8692,15 +7221,13 @@ snapshots: deep-extend@0.6.0: {} - deep-is@0.1.4: {} - deepmerge@4.3.1: {} default-browser-id@5.0.1: {} - default-browser@5.5.0: + default-browser@5.5.1: dependencies: - bundle-name: 4.1.0 + bundle-name: 4.1.1 default-browser-id: 5.0.1 define-data-property@1.1.4: @@ -8729,13 +7256,6 @@ snapshots: dettle@1.0.5: {} - diff@8.0.4: - optional: true - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - doctrine@3.0.0: dependencies: esutils: 2.0.3 @@ -8759,7 +7279,7 @@ snapshots: dependencies: domelementtype: 3.0.0 domhandler: 6.0.1 - entities: 8.0.0 + entities: 8.1.0 domelementtype@2.3.0: {} @@ -8788,7 +7308,7 @@ snapshots: draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/c58ebd9429a6359d72a88cff87e078aaf6fe285d(encoding@0.1.13)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: fbjs: 3.0.5(encoding@0.1.13) - immutable: 3.8.3 + immutable: 3.8.4 object-assign: 4.1.1 react: 19.3.0 react-dom: 19.3.0(react@19.3.0) @@ -8805,22 +7325,18 @@ snapshots: dependencies: '@one-ini/wasm': 0.2.1 commander: 14.0.3 - minimatch: 10.2.5 + minimatch: 10.2.6 semver: 7.8.5 ee-first@1.1.1: {} - electron-to-chromium@1.5.399: {} - - electron-to-chromium@1.5.439: {} + electron-to-chromium@1.5.446: {} emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - - empathic@2.0.1: {} + empathic@2.1.0: {} enabled@2.0.0: {} @@ -8836,7 +7352,7 @@ snapshots: entities@2.2.0: {} - entities@8.0.0: {} + entities@8.1.0: {} env-paths@2.2.1: {} @@ -8844,7 +7360,14 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-abstract@1.24.1: + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -8856,10 +7379,10 @@ snapshots: data-view-byte-offset: 1.0.1 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 get-intrinsic: 1.3.0 get-proto: 1.0.1 get-symbol-description: 1.1.0 @@ -8884,49 +7407,30 @@ snapshots: object-inspect: 1.13.4 object-keys: 1.1.1 object.assign: 4.1.7 - own-keys: 1.0.1 + own-keys: 1.0.2 regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.3 + safe-array-concat: 1.1.4 safe-push-apply: 1.0.0 safe-regex-test: 1.1.0 set-proto: 1.0.0 stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 string.prototype.trimstart: 1.0.8 typed-array-buffer: 1.0.3 typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 + typed-array-byte-offset: 1.0.5 + typed-array-length: 1.0.8 unbox-primitive: 1.1.0 - which-typed-array: 1.1.22 + which-typed-array: 1.1.24 es-define-property@1.0.1: {} es-errors@1.3.0: {} - es-iterator-helpers@1.2.2: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - safe-array-concat: 1.1.3 - es-module-lexer@2.3.2: {} - es-object-atoms@1.1.1: + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -8937,12 +7441,11 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.4 - - es-to-primitive@1.3.0: + es-to-primitive@1.3.4: dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 is-callable: 1.2.7 is-date-object: 1.1.0 is-symbol: 1.1.1 @@ -8982,170 +7485,8 @@ snapshots: escape-string-regexp@1.0.5: {} - escape-string-regexp@4.0.0: {} - - eslint-import-resolver-node@0.3.9(supports-color@10.2.2): - dependencies: - debug: 3.2.7(supports-color@10.2.2) - is-core-module: 2.16.2 - resolve: 1.22.12 - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9(supports-color@10.2.2))(eslint@9.39.2(supports-color@10.2.2))(supports-color@10.2.2): - dependencies: - debug: 3.2.7(supports-color@10.2.2) - optionalDependencies: - eslint: 9.39.2(supports-color@10.2.2) - eslint-import-resolver-node: 0.3.9(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.32.0(eslint@9.39.2(supports-color@10.2.2))(supports-color@10.2.2): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7(supports-color@10.2.2) - doctrine: 2.1.0 - eslint: 9.39.2(supports-color@10.2.2) - eslint-import-resolver-node: 0.3.9(supports-color@10.2.2) - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9(supports-color@10.2.2))(eslint@9.39.2(supports-color@10.2.2))(supports-color@10.2.2) - hasown: 2.0.4 - is-core-module: 2.16.2 - is-glob: 4.0.3 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(supports-color@10.2.2)): - dependencies: - aria-query: 5.3.2 - array-includes: 3.1.9 - array.prototype.flatmap: 1.3.3 - ast-types-flow: 0.0.8 - axe-core: 4.11.1 - axobject-query: 4.1.0 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - eslint: 9.39.2(supports-color@10.2.2) - hasown: 2.0.4 - jsx-ast-utils: 3.3.5 - language-tags: 1.0.9 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - safe-regex-test: 1.1.0 - string.prototype.includes: 2.0.1 - - eslint-plugin-react-hooks@7.1.1(eslint@9.39.2(supports-color@10.2.2))(supports-color@10.2.2): - dependencies: - '@babel/core': 7.29.7(supports-color@10.2.2) - '@babel/parser': 7.29.7 - eslint: 9.39.2(supports-color@10.2.2) - hermes-parser: 0.25.1 - zod: 4.3.6 - zod-validation-error: 4.0.2(zod@4.3.6) - transitivePeerDependencies: - - supports-color - - eslint-plugin-react@7.37.5(eslint@9.39.2(supports-color@10.2.2)): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.2 - eslint: 9.39.2(supports-color@10.2.2) - estraverse: 5.3.0 - hasown: 2.0.4 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.5 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint@9.39.2(supports-color@10.2.2): - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(supports-color@10.2.2)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.2(supports-color@10.2.2) - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.5(supports-color@10.2.2) - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.8 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.9 - ajv: 6.15.0 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@10.2.2) - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.18.0 - acorn-jsx: 5.3.2(acorn@8.18.0) - eslint-visitor-keys: 4.2.1 - esprima@4.0.1: {} - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -9170,7 +7511,7 @@ snapshots: dependencies: accepts: 2.0.0 body-parser: 2.3.0(supports-color@5.5.0) - content-disposition: 1.0.1 + content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 @@ -9187,19 +7528,19 @@ snapshots: on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.3 - range-parser: 1.2.1 + proxy-addr: 2.0.8 + qs: 6.16.0 + range-parser: 1.3.0 router: 2.2.0(supports-color@5.5.0) send: 1.2.1(supports-color@5.5.0) serve-static: 2.2.1(supports-color@5.5.0) statuses: 2.0.2 - type-is: 2.0.1 + type-is: 2.1.0 vary: 1.1.2 transitivePeerDependencies: - supports-color - exsolve@1.1.0: {} + exsolve@1.1.3: {} fancy-log@2.0.0: dependencies: @@ -9215,15 +7556,11 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fast-uri@3.1.5: {} + fast-uri@3.1.8: {} fastest-levenshtein@1.0.16: {} - fastq@1.20.1: + fastq@1.20.3: dependencies: reusify: 1.1.0 @@ -9241,9 +7578,9 @@ snapshots: transitivePeerDependencies: - encoding - fdir@6.5.0(picomatch@4.0.5): + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 fecha@4.2.3: {} @@ -9253,10 +7590,6 @@ snapshots: dependencies: flat-cache: 6.1.23 - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -9272,16 +7605,6 @@ snapshots: transitivePeerDependencies: - supports-color - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.4.4 - keyv: 4.5.4 - flat-cache@6.1.23: dependencies: cacheable: 2.5.0 @@ -9294,7 +7617,7 @@ snapshots: fn.name@1.1.0: {} - follow-redirects@1.16.0(debug@4.4.3(supports-color@5.5.0)): + follow-redirects@1.16.1(debug@4.4.3(supports-color@5.5.0)): optionalDependencies: debug: 4.4.3(supports-color@5.5.0) @@ -9318,13 +7641,6 @@ snapshots: fs-constants@1.0.0: {} - fs-extra@11.3.6: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.1 - universalify: 2.0.1 - optional: true - fs.realpath@1.0.0: {} fsevents@2.3.2: @@ -9335,14 +7651,17 @@ snapshots: function-bind@1.1.2: {} - function.prototype.name@1.1.8: + function.prototype.name@1.2.0: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 - define-properties: 1.2.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 hasown: 2.0.4 is-callable: 1.2.7 + is-document.all: 1.0.0 functions-have-names@1.2.3: {} @@ -9356,14 +7675,14 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.6.0: {} + get-east-asian-width@1.7.0: {} get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 @@ -9374,7 +7693,7 @@ snapshots: get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-symbol-description@1.1.0: dependencies: @@ -9395,11 +7714,7 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob-to-regex.js@1.2.0(tslib@2.8.1): + glob-to-regex.js@1.3.1(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -9407,7 +7722,7 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.6 minipass: 7.1.3 path-scurry: 2.0.2 @@ -9430,8 +7745,6 @@ snapshots: kind-of: 6.0.3 which: 1.3.1 - globals@14.0.0: {} - globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -9441,11 +7754,11 @@ snapshots: dependencies: '@sindresorhus/merge-streams': 4.0.0 fast-glob: 3.3.3 - ignore: 7.0.6 + ignore: 7.0.12 is-path-inside: 4.0.0 micromatch: 4.0.8 slash: 5.1.0 - unicorn-magic: 0.4.0 + unicorn-magic: 0.4.1 globjoin@0.1.4: {} @@ -9483,12 +7796,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hermes-estree@0.25.1: {} - - hermes-parser@0.25.1: - dependencies: - hermes-estree: 0.25.1 - highlight.js@11.12.0: {} hookified@1.15.1: {} @@ -9499,7 +7806,7 @@ snapshots: html-encoding-sniffer@7.0.0: dependencies: - '@exodus/bytes': 1.15.1 + '@exodus/bytes': 1.16.0 transitivePeerDependencies: - '@noble/hashes' @@ -9526,23 +7833,21 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.7.2: + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.28): + icss-utils@5.1.0(postcss@8.5.29): dependencies: - postcss: 8.5.28 + postcss: 8.5.29 ieee754@1.2.1: {} ignore-by-default@1.0.1: {} - ignore@5.3.2: {} + ignore@7.0.12: {} - ignore@7.0.6: {} - - immutable@3.8.3: {} + immutable@3.8.4: {} immutable@5.1.9: {} @@ -9551,13 +7856,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-lazy@4.0.0: - optional: true - import-meta-resolve@4.2.0: {} - imurmurhash@0.1.4: {} - indent-string@4.0.0: {} inflight@1.0.6: @@ -9615,7 +7915,7 @@ snapshots: is-callable@1.2.7: {} - is-core-module@2.16.2: + is-core-module@2.17.0: dependencies: hasown: 2.0.4 @@ -9632,6 +7932,10 @@ snapshots: is-docker@3.0.0: {} + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -9712,7 +8016,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.22 + which-typed-array: 1.1.24 is-weakmap@2.0.2: {} @@ -9735,19 +8039,7 @@ snapshots: isexe@2.0.0: {} - iterator.prototype@1.1.5: - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 - - jju@1.4.0: - optional: true - - joi@18.2.3: + joi@18.2.9: dependencies: '@hapi/address': 5.1.1 '@hapi/formula': 3.0.2 @@ -9771,23 +8063,23 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.3.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 - jsdom@30.1.1(canvas@3.2.3): + jsdom@30.1.2(canvas@3.2.3): dependencies: - '@asamuzakjp/css-color': 7.1.2 - '@asamuzakjp/dom-selector': 9.2.2 - '@bramus/specificity': 2.4.2 - '@csstools/css-syntax-patches-for-csstree': 1.1.14(css-tree@3.2.1) - '@exodus/bytes': 1.15.1 + '@asamuzakjp/css-color': 7.1.3 + '@asamuzakjp/dom-selector': 9.2.4 + '@bramus/specificity': 2.4.3 + '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1) + '@exodus/bytes': 1.16.0 css-tree: 3.2.1 - data-urls: 7.0.0 + data-urls: 8.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 7.0.0 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.5.2 + lru-cache: 11.5.3 parse5: 8.0.1 saxes: 6.0.0 tough-cookie: 6.0.2 @@ -9795,7 +8087,7 @@ snapshots: w3c-xmlserializer: 6.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 17.1.2 + whatwg-url: 17.2.0 xml-name-validator: 5.0.0 optionalDependencies: canvas: 3.2.3 @@ -9804,44 +8096,16 @@ snapshots: jsesc@3.1.0: {} - json-buffer@3.0.1: {} - json-parse-better-errors@1.0.2: {} json-parse-even-better-errors@2.3.1: {} - json-schema-traverse@0.4.1: {} - json-schema-traverse@1.0.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@1.0.2: - dependencies: - minimist: 1.2.8 - json5@2.2.3: {} jsonc-parser@3.3.1: {} - jsonfile@6.2.1: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - optional: true - - jsx-ast-utils@3.3.5: - dependencies: - array-includes: 3.1.9 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - keyv@5.6.0: dependencies: '@keyv/serialize': 1.1.1 @@ -9854,17 +8118,6 @@ snapshots: kuler@2.0.0: {} - language-subtag-registry@0.3.23: {} - - language-tags@1.0.9: - dependencies: - language-subtag-registry: 0.3.23 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - lightningcss-android-arm64@1.33.0: optional: true @@ -9928,13 +8181,9 @@ snapshots: local-pkg@1.2.1: dependencies: mlly: 1.8.2 - pkg-types: 2.3.1 + pkg-types: 2.3.3 quansync: 0.2.11 - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - lodash.camelcase@4.3.0: {} lodash.debounce@4.0.8: {} @@ -9962,38 +8211,31 @@ snapshots: loupe@3.2.1: {} - lru-cache@11.5.2: {} - lru-cache@11.5.3: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - optional: true - lz-string@1.5.0: {} magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 - magic-string@1.4.2: + magic-string@1.4.3: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 - magicast@0.5.4: + magicast@0.5.5: dependencies: - '@babel/parser': 7.29.8 + '@babel/parser': 7.29.9 '@babel/types': 7.29.8 - source-map-js: 1.2.1 + source-map-js: 1.2.2 map-stream@0.0.7: {} - marked@18.0.14: {} + marked@18.1.0: {} math-intrinsics@1.1.0: {} @@ -10005,21 +8247,21 @@ snapshots: mdn-data@2.27.1: {} - media-typer@1.1.0: {} + media-typer@1.1.1: {} - memfs@4.64.0(tslib@2.8.1): + memfs@4.80.0: dependencies: - '@jsonjoy.com/fs-core': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-fsa': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-to-fsa': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-print': 4.64.0(tslib@2.8.1) - '@jsonjoy.com/fs-snapshot': 4.64.0(tslib@2.8.1) + '@jsonjoy.com/fs-core': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-fsa': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-to-fsa': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.80.0(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.80.0(tslib@2.8.1) '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) - glob-to-regex.js: 1.2.0(tslib@2.8.1) + glob-to-regex.js: 1.3.1(tslib@2.8.1) thingies: 2.6.1(tslib@2.8.1) tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 @@ -10053,13 +8295,13 @@ snapshots: min-indent@1.0.1: {} - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.18 + brace-expansion: 1.1.21 minimist@1.2.8: {} @@ -10071,7 +8313,7 @@ snapshots: mlly@1.8.2: dependencies: - acorn: 8.18.0 + acorn: 8.19.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.4 @@ -10084,19 +8326,19 @@ snapshots: mustache@4.2.0: {} - nanoid@3.3.18: {} + nanoid@3.3.20: {} napi-build-utils@2.0.0: {} - natural-compare@1.4.0: {} - negotiator@0.6.4: {} - negotiator@1.0.0: {} + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 nice-try@1.0.5: {} - node-abi@3.93.0: + node-abi@3.97.0: dependencies: semver: 7.8.5 @@ -10108,8 +8350,6 @@ snapshots: optionalDependencies: encoding: 0.1.13 - node-releases@2.0.51: {} - node-releases@2.0.57: {} nodemon@3.1.14: @@ -10117,7 +8357,7 @@ snapshots: chokidar: 3.6.0 debug: 4.4.3(supports-color@5.5.0) ignore-by-default: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 pstree.remy: 1.1.8 semver: 7.8.5 simple-update-notifier: 2.0.0 @@ -10147,7 +8387,7 @@ snapshots: minimatch: 3.1.5 pidtree: 0.3.1 read-pkg: 3.0.0 - shell-quote: 1.9.0 + shell-quote: 1.12.0 string.prototype.padend: 3.1.6 nth-check@2.1.1: @@ -10174,38 +8414,11 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 has-symbols: 1.1.0 object-keys: 1.1.1 - object.entries@1.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-object-atoms: 1.1.1 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.1 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - obug@2.1.4: {} + obug@2.2.1: {} on-finished@2.4.1: dependencies: @@ -10223,24 +8436,16 @@ snapshots: open@10.2.0: dependencies: - default-browser: 5.5.0 + default-browser: 5.5.1 define-lazy-prop: 3.0.0 is-inside-container: 1.0.0 wsl-utils: 0.1.0 opentype.js@2.0.0: {} - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - own-keys@1.0.1: + own-keys@1.0.2: dependencies: + call-bound: 1.0.4 get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 @@ -10292,18 +8497,10 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.21.2 '@oxc-resolver/binding-win32-x64-msvc': 11.21.2 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - p-limit@7.3.3: dependencies: yocto-queue: 1.2.2 - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - package-json-from-dist@1.0.1: {} parent-module@1.0.1: @@ -10324,25 +8521,21 @@ snapshots: parse5@8.0.1: dependencies: - entities: 8.0.0 + entities: 8.1.0 parseurl@1.3.3: {} path-browserify@1.0.1: {} - path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} path-key@2.0.1: {} - path-key@3.1.1: {} - path-parse@1.0.7: {} path-scurry@2.0.2: dependencies: - lru-cache: 11.5.2 + lru-cache: 11.5.3 minipass: 7.1.3 path-to-regexp@8.4.2: {} @@ -10370,8 +8563,6 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.5: {} - picomatch@4.0.7: {} pidtree@0.3.1: {} @@ -10384,10 +8575,10 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 - pkg-types@2.3.1: + pkg-types@2.3.3: dependencies: - confbox: 0.2.4 - exsolve: 1.1.0 + confbox: 0.3.1 + exsolve: 1.1.3 pathe: 2.0.3 playwright-core@1.62.1: {} @@ -10413,53 +8604,48 @@ snapshots: postcss-media-query-parser@0.2.3: {} - postcss-modules-extract-imports@3.1.0(postcss@8.5.28): + postcss-modules-extract-imports@3.1.0(postcss@8.5.29): dependencies: - postcss: 8.5.28 + postcss: 8.5.29 - postcss-modules-local-by-default@4.2.0(postcss@8.5.28): + postcss-modules-local-by-default@4.2.0(postcss@8.5.29): dependencies: - icss-utils: 5.1.0(postcss@8.5.28) - postcss: 8.5.28 - postcss-selector-parser: 7.1.4 + icss-utils: 5.1.0(postcss@8.5.29) + postcss: 8.5.29 + postcss-selector-parser: 7.1.6 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.28): + postcss-modules-scope@3.2.1(postcss@8.5.29): dependencies: - postcss: 8.5.28 - postcss-selector-parser: 7.1.4 + postcss: 8.5.29 + postcss-selector-parser: 7.1.6 - postcss-modules-values@4.0.0(postcss@8.5.28): + postcss-modules-values@4.0.0(postcss@8.5.29): dependencies: - icss-utils: 5.1.0(postcss@8.5.28) - postcss: 8.5.28 + icss-utils: 5.1.0(postcss@8.5.29) + postcss: 8.5.29 - postcss-modules@9.0.1(postcss@8.5.28): + postcss-modules@9.0.1(postcss@8.5.29): dependencies: generic-names: 4.0.0 - icss-utils: 5.1.0(postcss@8.5.28) + icss-utils: 5.1.0(postcss@8.5.29) lodash.camelcase: 4.3.0 - postcss: 8.5.28 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.28) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.28) - postcss-modules-scope: 3.2.1(postcss@8.5.28) - postcss-modules-values: 4.0.0(postcss@8.5.28) + postcss: 8.5.29 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.29) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.29) + postcss-modules-scope: 3.2.1(postcss@8.5.29) + postcss-modules-values: 4.0.0(postcss@8.5.29) string-hash: 1.1.3 postcss-resolve-nested-selector@0.1.6: {} - postcss-safe-parser@7.0.1(postcss@8.5.28): + postcss-safe-parser@7.1.0(postcss@8.5.29): dependencies: - postcss: 8.5.28 + postcss: 8.5.29 - postcss-scss@4.0.9(postcss@8.5.28): + postcss-scss@4.0.9(postcss@8.5.29): dependencies: - postcss: 8.5.28 - - postcss-selector-parser@7.1.4: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 + postcss: 8.5.29 postcss-selector-parser@7.1.6: dependencies: @@ -10475,11 +8661,11 @@ snapshots: picocolors: 0.2.1 source-map: 0.6.1 - postcss@8.5.28: + postcss@8.5.29: dependencies: - nanoid: 3.3.18 + nanoid: 3.3.20 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 prebuild-install@7.1.3: dependencies: @@ -10489,15 +8675,13 @@ snapshots: minimist: 1.2.8 mkdirp-classic: 0.5.3 napi-build-utils: 2.0.0 - node-abi: 3.93.0 + node-abi: 3.97.0 pump: 3.0.4 rc: 1.2.8 simple-get: 4.0.1 tar-fs: 2.1.5 tunnel-agent: 0.6.0 - prelude-ls@1.2.1: {} - prettier@3.9.9: {} pretty-format@27.5.1: @@ -10532,7 +8716,7 @@ snapshots: proto-list@1.2.4: {} - proxy-addr@2.0.7: + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 @@ -10554,7 +8738,7 @@ snapshots: dependencies: hookified: 2.2.0 - qs@6.15.3: + qs@6.16.0: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 @@ -10565,13 +8749,13 @@ snapshots: randomcolor@0.6.2: {} - range-parser@1.2.1: {} + range-parser@1.3.0: {} raw-body@3.0.2: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 unpipe: 1.0.0 rc@1.2.8: @@ -10605,7 +8789,7 @@ snapshots: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) react-stately: 3.50.0(react@19.3.0) - use-sync-external-store: 1.6.0(react@19.3.0) + use-sync-external-store: 1.7.0(react@19.3.0) react-compiler-runtime@1.0.0(react@19.3.0): dependencies: @@ -10615,7 +8799,7 @@ snapshots: dependencies: typescript: 6.0.3 - react-docgen@8.0.3(supports-color@10.2.2): + react-docgen@8.0.4(supports-color@10.2.2): dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/traverse': 7.29.8(supports-color@10.2.2) @@ -10630,7 +8814,7 @@ snapshots: transitivePeerDependencies: - supports-color - react-docgen@8.0.3(supports-color@5.5.0): + react-docgen@8.0.4(supports-color@5.5.0): dependencies: '@babel/core': 7.29.7(supports-color@5.5.0) '@babel/traverse': 7.29.8(supports-color@5.5.0) @@ -10670,7 +8854,7 @@ snapshots: '@react-types/shared': 3.36.1(react@19.3.0) '@swc/helpers': 0.5.23 react: 19.3.0 - use-sync-external-store: 1.6.0(react@19.3.0) + use-sync-external-store: 1.7.0(react@19.3.0) react-virtualized@9.22.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: @@ -10711,13 +8895,13 @@ snapshots: dependencies: picomatch: 2.3.2 - readdirp@5.0.0: {} + readdirp@5.1.1: {} readline-sync@1.4.10: {} - recast@0.23.19: + recast@0.23.21: dependencies: - ast-types: 0.16.1 + ast-types: 0.16.3 esprima: 4.0.1 source-map: 0.6.1 tiny-invariant: 1.3.3 @@ -10732,9 +8916,9 @@ snapshots: dependencies: call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 @@ -10761,13 +8945,7 @@ snapshots: resolve@1.22.12: dependencies: es-errors: 1.3.0 - is-core-module: 2.16.2 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - resolve@2.0.0-next.5: - dependencies: - is-core-module: 2.16.2 + is-core-module: 2.17.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -10778,80 +8956,26 @@ snapshots: glob: 13.0.6 package-json-from-dist: 1.0.1 - rolldown@1.2.1: + rolldown@1.2.12: dependencies: - '@oxc-project/types': 0.142.0 + '@oxc-project/types': 0.152.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.1 - '@rolldown/binding-darwin-arm64': 1.2.1 - '@rolldown/binding-darwin-x64': 1.2.1 - '@rolldown/binding-freebsd-x64': 1.2.1 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.1 - '@rolldown/binding-linux-arm64-gnu': 1.2.1 - '@rolldown/binding-linux-arm64-musl': 1.2.1 - '@rolldown/binding-linux-ppc64-gnu': 1.2.1 - '@rolldown/binding-linux-s390x-gnu': 1.2.1 - '@rolldown/binding-linux-x64-gnu': 1.2.1 - '@rolldown/binding-linux-x64-musl': 1.2.1 - '@rolldown/binding-openharmony-arm64': 1.2.1 - '@rolldown/binding-wasm32-wasi': 1.2.1 - '@rolldown/binding-win32-arm64-msvc': 1.2.1 - '@rolldown/binding-win32-x64-msvc': 1.2.1 - optional: true - - rolldown@1.2.11: - dependencies: - '@oxc-project/types': 0.151.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.11 - '@rolldown/binding-android-arm64': 1.2.11 - '@rolldown/binding-darwin-arm64': 1.2.11 - '@rolldown/binding-darwin-x64': 1.2.11 - '@rolldown/binding-freebsd-x64': 1.2.11 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.11 - '@rolldown/binding-linux-arm64-gnu': 1.2.11 - '@rolldown/binding-linux-arm64-musl': 1.2.11 - '@rolldown/binding-linux-ppc64-gnu': 1.2.11 - '@rolldown/binding-linux-s390x-gnu': 1.2.11 - '@rolldown/binding-linux-x64-gnu': 1.2.11 - '@rolldown/binding-linux-x64-musl': 1.2.11 - '@rolldown/binding-openharmony-arm64': 1.2.11 - '@rolldown/binding-win32-arm64-msvc': 1.2.11 - '@rolldown/binding-win32-x64-msvc': 1.2.11 - - rollup@4.61.1: - dependencies: - '@types/estree': 1.0.9 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.61.1 - '@rollup/rollup-android-arm64': 4.61.1 - '@rollup/rollup-darwin-arm64': 4.61.1 - '@rollup/rollup-darwin-x64': 4.61.1 - '@rollup/rollup-freebsd-arm64': 4.61.1 - '@rollup/rollup-freebsd-x64': 4.61.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.61.1 - '@rollup/rollup-linux-arm-musleabihf': 4.61.1 - '@rollup/rollup-linux-arm64-gnu': 4.61.1 - '@rollup/rollup-linux-arm64-musl': 4.61.1 - '@rollup/rollup-linux-loong64-gnu': 4.61.1 - '@rollup/rollup-linux-loong64-musl': 4.61.1 - '@rollup/rollup-linux-ppc64-gnu': 4.61.1 - '@rollup/rollup-linux-ppc64-musl': 4.61.1 - '@rollup/rollup-linux-riscv64-gnu': 4.61.1 - '@rollup/rollup-linux-riscv64-musl': 4.61.1 - '@rollup/rollup-linux-s390x-gnu': 4.61.1 - '@rollup/rollup-linux-x64-gnu': 4.61.1 - '@rollup/rollup-linux-x64-musl': 4.61.1 - '@rollup/rollup-openbsd-x64': 4.61.1 - '@rollup/rollup-openharmony-arm64': 4.61.1 - '@rollup/rollup-win32-arm64-msvc': 4.61.1 - '@rollup/rollup-win32-ia32-msvc': 4.61.1 - '@rollup/rollup-win32-x64-gnu': 4.61.1 - '@rollup/rollup-win32-x64-msvc': 4.61.1 - fsevents: 2.3.3 - optional: true + '@rolldown/binding-android-arm-eabi': 1.2.12 + '@rolldown/binding-android-arm64': 1.2.12 + '@rolldown/binding-darwin-arm64': 1.2.12 + '@rolldown/binding-darwin-x64': 1.2.12 + '@rolldown/binding-freebsd-x64': 1.2.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.12 + '@rolldown/binding-linux-arm64-gnu': 1.2.12 + '@rolldown/binding-linux-arm64-musl': 1.2.12 + '@rolldown/binding-linux-ppc64-gnu': 1.2.12 + '@rolldown/binding-linux-s390x-gnu': 1.2.12 + '@rolldown/binding-linux-x64-gnu': 1.2.12 + '@rolldown/binding-linux-x64-musl': 1.2.12 + '@rolldown/binding-openharmony-arm64': 1.2.12 + '@rolldown/binding-win32-arm64-msvc': 1.2.12 + '@rolldown/binding-win32-x64-msvc': 1.2.12 router@2.2.0(supports-color@5.5.0): dependencies: @@ -10875,7 +8999,7 @@ snapshots: rxjs@8.0.0-alpha.14: {} - safe-array-concat@1.1.3: + safe-array-concat@1.1.4: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 @@ -10902,67 +9026,67 @@ snapshots: safer-buffer@2.1.2: {} - sass-embedded-all-unknown@1.105.0: + sass-embedded-all-unknown@1.105.1: dependencies: - sass: 1.105.0 + sass: 1.105.1 optional: true - sass-embedded-android-arm64@1.105.0: + sass-embedded-android-arm64@1.105.1: optional: true - sass-embedded-android-arm@1.105.0: + sass-embedded-android-arm@1.105.1: optional: true - sass-embedded-android-riscv64@1.105.0: + sass-embedded-android-riscv64@1.105.1: optional: true - sass-embedded-android-x64@1.105.0: + sass-embedded-android-x64@1.105.1: optional: true - sass-embedded-darwin-arm64@1.105.0: + sass-embedded-darwin-arm64@1.105.1: optional: true - sass-embedded-darwin-x64@1.105.0: + sass-embedded-darwin-x64@1.105.1: optional: true - sass-embedded-linux-arm64@1.105.0: + sass-embedded-linux-arm64@1.105.1: optional: true - sass-embedded-linux-arm@1.105.0: + sass-embedded-linux-arm@1.105.1: optional: true - sass-embedded-linux-musl-arm64@1.105.0: + sass-embedded-linux-musl-arm64@1.105.1: optional: true - sass-embedded-linux-musl-arm@1.105.0: + sass-embedded-linux-musl-arm@1.105.1: optional: true - sass-embedded-linux-musl-riscv64@1.105.0: + sass-embedded-linux-musl-riscv64@1.105.1: optional: true - sass-embedded-linux-musl-x64@1.105.0: + sass-embedded-linux-musl-x64@1.105.1: optional: true - sass-embedded-linux-riscv64@1.105.0: + sass-embedded-linux-riscv64@1.105.1: optional: true - sass-embedded-linux-x64@1.105.0: + sass-embedded-linux-x64@1.105.1: optional: true - sass-embedded-unknown-all@1.105.0: + sass-embedded-unknown-all@1.105.1: dependencies: - sass: 1.105.0 + sass: 1.105.1 optional: true - sass-embedded-win32-arm64@1.105.0: + sass-embedded-win32-arm64@1.105.1: optional: true - sass-embedded-win32-x64@1.105.0: + sass-embedded-win32-x64@1.105.1: optional: true - sass-embedded@1.105.0: + sass-embedded@1.105.1: dependencies: - '@bufbuild/protobuf': 2.12.1 + '@bufbuild/protobuf': 2.16.0 colorjs.io: 0.7.1 immutable: 5.1.9 rxjs: 7.8.2 @@ -10970,30 +9094,30 @@ snapshots: sync-child-process: 1.0.2 varint: 6.0.0 optionalDependencies: - sass-embedded-all-unknown: 1.105.0 - sass-embedded-android-arm: 1.105.0 - sass-embedded-android-arm64: 1.105.0 - sass-embedded-android-riscv64: 1.105.0 - sass-embedded-android-x64: 1.105.0 - sass-embedded-darwin-arm64: 1.105.0 - sass-embedded-darwin-x64: 1.105.0 - sass-embedded-linux-arm: 1.105.0 - sass-embedded-linux-arm64: 1.105.0 - sass-embedded-linux-musl-arm: 1.105.0 - sass-embedded-linux-musl-arm64: 1.105.0 - sass-embedded-linux-musl-riscv64: 1.105.0 - sass-embedded-linux-musl-x64: 1.105.0 - sass-embedded-linux-riscv64: 1.105.0 - sass-embedded-linux-x64: 1.105.0 - sass-embedded-unknown-all: 1.105.0 - sass-embedded-win32-arm64: 1.105.0 - sass-embedded-win32-x64: 1.105.0 + sass-embedded-all-unknown: 1.105.1 + sass-embedded-android-arm: 1.105.1 + sass-embedded-android-arm64: 1.105.1 + sass-embedded-android-riscv64: 1.105.1 + sass-embedded-android-x64: 1.105.1 + sass-embedded-darwin-arm64: 1.105.1 + sass-embedded-darwin-x64: 1.105.1 + sass-embedded-linux-arm: 1.105.1 + sass-embedded-linux-arm64: 1.105.1 + sass-embedded-linux-musl-arm: 1.105.1 + sass-embedded-linux-musl-arm64: 1.105.1 + sass-embedded-linux-musl-riscv64: 1.105.1 + sass-embedded-linux-musl-x64: 1.105.1 + sass-embedded-linux-riscv64: 1.105.1 + sass-embedded-linux-x64: 1.105.1 + sass-embedded-unknown-all: 1.105.1 + sass-embedded-win32-arm64: 1.105.1 + sass-embedded-win32-x64: 1.105.1 - sass@1.105.0: + sass@1.105.1: dependencies: chokidar: 5.0.0 immutable: 5.1.9 - source-map-js: 1.2.1 + source-map-js: 1.2.2 optionalDependencies: '@parcel/watcher': 2.6.0 @@ -11009,11 +9133,6 @@ snapshots: semver@6.3.1: {} - semver@7.5.4: - dependencies: - lru-cache: 6.0.0 - optional: true - semver@7.8.5: {} send@1.2.1(supports-color@5.5.0): @@ -11027,7 +9146,7 @@ snapshots: mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 - range-parser: 1.2.1 + range-parser: 1.3.0 statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -11061,7 +9180,7 @@ snapshots: dependencies: dunder-proto: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 setimmediate@1.0.5: {} @@ -11071,15 +9190,9 @@ snapshots: dependencies: shebang-regex: 1.0.0 - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - shebang-regex@1.0.0: {} - shebang-regex@3.0.0: {} - - shell-quote@1.9.0: {} + shell-quote@1.12.0: {} side-channel-list@1.0.1: dependencies: @@ -11137,7 +9250,7 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - source-map-js@1.2.1: {} + source-map-js@1.2.2: {} source-map-support@0.5.21: dependencies: @@ -11149,19 +9262,16 @@ snapshots: spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.22 + spdx-license-ids: 3.0.24 spdx-exceptions@2.5.0: {} spdx-expression-parse@3.0.1: dependencies: spdx-exceptions: 2.5.0 - spdx-license-ids: 3.0.22 + spdx-license-ids: 3.0.24 - spdx-license-ids@3.0.22: {} - - sprintf-js@1.0.3: - optional: true + spdx-license-ids@3.0.24: {} stable@0.1.8: {} @@ -11169,20 +9279,20 @@ snapshots: statuses@2.0.2: {} - std-env@4.2.0: {} + std-env@4.3.0: {} stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@10.6.0(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0): + storybook@10.6.1(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.1.0(react@19.3.0) '@testing-library/dom': 10.4.2 '@testing-library/jest-dom': 6.9.1 - '@testing-library/user-event': 14.6.3(@testing-library/dom@10.4.2) + '@testing-library/user-event': 14.6.7(@testing-library/dom@10.4.2) '@vitest/expect': 3.2.4 '@vitest/spy': 3.2.4 '@webcontainer/env': 1.1.1 @@ -11191,10 +9301,10 @@ snapshots: open: 10.2.0 oxc-parser: 0.127.0 oxc-resolver: 11.21.2 - recast: 0.23.19 + recast: 0.23.21 semver: 7.8.5 - use-sync-external-store: 1.6.0(react@19.3.0) - ws: 8.21.2 + use-sync-external-store: 1.7.0(react@19.3.0) + ws: 8.22.0 optionalDependencies: '@types/react': 19.3.0 prettier: 3.9.9 @@ -11207,9 +9317,6 @@ snapshots: dependencies: component-emitter: 2.0.0 - string-argv@0.3.2: - optional: true - string-hash@1.1.3: {} string-width@4.2.3: @@ -11221,70 +9328,44 @@ snapshots: string-width@7.2.0: dependencies: emoji-regex: 10.6.0 - get-east-asian-width: 1.6.0 + get-east-asian-width: 1.7.0 strip-ansi: 7.2.0 - string-width@8.2.2: + string-width@8.3.0: dependencies: - get-east-asian-width: 1.6.0 + get-east-asian-width: 1.7.0 strip-ansi: 7.2.0 - string.prototype.includes@2.0.1: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.1 - - string.prototype.matchall@4.0.12: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.1 - string.prototype.padend@3.1.6: dependencies: call-bind: 1.0.9 define-properties: 1.2.1 - es-abstract: 1.24.1 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 - string.prototype.repeat@1.0.0: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.24.1 - - string.prototype.trim@1.2.10: + string.prototype.trim@1.2.11: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.1 - es-object-atoms: 1.1.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 - string.prototype.trimend@1.0.9: + string.prototype.trimend@1.0.10: dependencies: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string.prototype.trimstart@1.0.8: dependencies: call-bind: 1.0.9 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 string_decoder@1.1.1: dependencies: @@ -11300,7 +9381,7 @@ snapshots: strip-ansi@7.2.0: dependencies: - ansi-regex: 6.2.2 + ansi-regex: 6.4.0 strip-bom@3.0.0: {} @@ -11312,15 +9393,13 @@ snapshots: strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} - stubborn-fs@1.2.5: {} - style-dictionary@5.5.5(tslib@2.8.1): + style-dictionary@5.5.5: dependencies: '@bundled-es-modules/deepmerge': 4.3.2 '@bundled-es-modules/glob': 13.0.6 - '@bundled-es-modules/memfs': 4.17.0(tslib@2.8.1) + '@bundled-es-modules/memfs': 4.17.0 '@zip.js/zip.js': 2.8.34(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95) chalk: 5.6.2 change-case: 5.4.4 @@ -11331,45 +9410,43 @@ snapshots: path-unified: 0.2.0 prettier: 3.9.9 tinycolor2: 1.6.0 - transitivePeerDependencies: - - tslib - stylelint-config-recommended-scss@17.0.1(postcss@8.5.28)(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)): + stylelint-config-recommended-scss@17.0.1(postcss@8.5.29)(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - postcss-scss: 4.0.9(postcss@8.5.28) - stylelint: 17.15.0(supports-color@5.5.0)(typescript@6.0.3) - stylelint-config-recommended: 18.0.0(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)) - stylelint-scss: 7.3.0(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)) + postcss-scss: 4.0.9(postcss@8.5.29) + stylelint: 17.16.0(supports-color@5.5.0)(typescript@6.0.3) + stylelint-config-recommended: 18.0.0(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)) + stylelint-scss: 7.3.0(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)) optionalDependencies: - postcss: 8.5.28 + postcss: 8.5.29 - stylelint-config-recommended@18.0.0(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)): + stylelint-config-recommended@18.0.0(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - stylelint: 17.15.0(supports-color@5.5.0)(typescript@6.0.3) + stylelint: 17.16.0(supports-color@5.5.0)(typescript@6.0.3) - stylelint-config-standard-scss@17.0.0(postcss@8.5.28)(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)): + stylelint-config-standard-scss@17.0.0(postcss@8.5.29)(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - stylelint: 17.15.0(supports-color@5.5.0)(typescript@6.0.3) - stylelint-config-recommended-scss: 17.0.1(postcss@8.5.28)(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)) - stylelint-config-standard: 40.0.0(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)) + stylelint: 17.16.0(supports-color@5.5.0)(typescript@6.0.3) + stylelint-config-recommended-scss: 17.0.1(postcss@8.5.29)(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)) + stylelint-config-standard: 40.0.0(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)) optionalDependencies: - postcss: 8.5.28 + postcss: 8.5.29 - stylelint-config-standard@40.0.0(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)): + stylelint-config-standard@40.0.0(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - stylelint: 17.15.0(supports-color@5.5.0)(typescript@6.0.3) - stylelint-config-recommended: 18.0.0(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)) + stylelint: 17.16.0(supports-color@5.5.0)(typescript@6.0.3) + stylelint-config-recommended: 18.0.0(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)) - stylelint-plugin-logical-css@2.1.0(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)): + stylelint-plugin-logical-css@2.1.0(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - stylelint: 17.15.0(supports-color@5.5.0)(typescript@6.0.3) + stylelint: 17.16.0(supports-color@5.5.0)(typescript@6.0.3) - stylelint-scss@7.3.0(stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3)): + stylelint-scss@7.3.0(stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3)): dependencies: - '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.14(css-tree@3.2.1) - '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1) + '@csstools/css-tokenizer': 4.0.2 css-tree: 3.2.1 is-plain-object: 5.1.0 known-css-properties: 0.37.0 @@ -11377,17 +9454,17 @@ snapshots: postcss-resolve-nested-selector: 0.1.6 postcss-selector-parser: 7.1.6 postcss-value-parser: 4.2.0 - stylelint: 17.15.0(supports-color@5.5.0)(typescript@6.0.3) + stylelint: 17.16.0(supports-color@5.5.0)(typescript@6.0.3) - stylelint@17.15.0(supports-color@5.5.0)(typescript@6.0.3): + stylelint@17.16.0(supports-color@5.5.0)(typescript@6.0.3): dependencies: - '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.14(css-tree@3.2.1) - '@csstools/css-tokenizer': 4.0.0 - '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/selector-resolve-nested': 4.0.1(postcss-selector-parser@7.1.6) - '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.6) + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1) + '@csstools/css-tokenizer': 4.0.2 + '@csstools/media-query-list-parser': 5.0.2(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/selector-resolve-nested': 4.0.2(postcss-selector-parser@7.1.6) + '@csstools/selector-specificity': 6.0.1(postcss-selector-parser@7.1.6) colord: 2.10.0 cosmiconfig: 9.0.2(typescript@6.0.3) css-functions-list: 3.3.3 @@ -11400,19 +9477,19 @@ snapshots: globby: 16.2.4 globjoin: 0.1.4 html-tags: 5.1.0 - ignore: 7.0.6 + ignore: 7.0.12 import-meta-resolve: 4.2.0 mathml-tag-names: 4.0.0 meow: 14.1.0 micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.28 - postcss-safe-parser: 7.0.1(postcss@8.5.28) + postcss: 8.5.29 + postcss-safe-parser: 7.1.0(postcss@8.5.29) postcss-selector-parser: 7.1.6 postcss-value-parser: 4.2.0 - string-width: 8.2.2 - supports-hyperlinks: 4.5.0 + string-width: 8.3.0 + supports-hyperlinks: 4.6.0 svg-tags: 1.0.0 table: 6.9.0 write-file-atomic: 7.0.1 @@ -11426,15 +9503,11 @@ snapshots: dependencies: has-flag: 3.0.0 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - supports-color@8.1.1: dependencies: has-flag: 4.0.0 - supports-hyperlinks@4.5.0: + supports-hyperlinks@4.6.0: dependencies: has-flag: 5.0.1 supports-color: 10.2.2 @@ -11444,26 +9517,26 @@ snapshots: svg-sprite@2.0.4: dependencies: '@resvg/resvg-js': 2.6.2 - '@xmldom/xmldom': 0.8.13 + '@xmldom/xmldom': 0.8.15 async: 3.2.6 css-selector-parser: 1.4.1 csso: 4.2.0 cssom: 0.5.0 glob: 7.2.3 - js-yaml: 4.3.0 + js-yaml: 4.3.2 lodash.escape: 4.0.1 lodash.merge: 4.6.2 mustache: 4.2.0 prettysize: 2.0.0 - svgo: 2.8.3 + svgo: 2.8.4 vinyl: 2.2.1 winston: 3.19.0 xpath: 0.0.34 - yargs: 17.7.2 + yargs: 17.7.3 svg-tags@1.0.0: {} - svgo@2.8.3: + svgo@2.8.4: dependencies: commander: 7.2.0 css-select: 4.3.0 @@ -11518,7 +9591,7 @@ snapshots: dependencies: promise-make-counter: 1.0.2 - tinybench@6.2.0: {} + tinybench@6.2.1: {} tinycolor2@1.6.0: {} @@ -11526,20 +9599,20 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 tinyrainbow@2.0.0: {} - tinyrainbow@3.1.1: {} + tinyrainbow@3.2.0: {} - tinyspy@4.0.4: {} + tinyspy@4.0.6: {} - tldts-core@7.4.10: {} + tldts-core@7.4.16: {} - tldts@7.4.10: + tldts@7.4.16: dependencies: - tldts-core: 7.4.10 + tldts-core: 7.4.16 to-regex-range@5.0.1: dependencies: @@ -11553,11 +9626,11 @@ snapshots: tough-cookie@6.0.2: dependencies: - tldts: 7.4.10 + tldts: 7.4.16 tr46@0.0.3: {} - tr46@6.0.0: + tr46@7.0.0: dependencies: punycode: 2.3.1 @@ -11571,13 +9644,6 @@ snapshots: ts-dedent@2.3.0: {} - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -11590,20 +9656,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-is@2.0.1: - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.2 - type-is@2.1.0: dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 + content-type: 2.1.0 + media-typer: 1.1.1 mime-types: 3.0.2 typed-array-buffer@1.0.3: @@ -11620,17 +9676,16 @@ snapshots: has-proto: 1.2.0 is-typed-array: 1.1.15 - typed-array-byte-offset@1.0.4: + typed-array-byte-offset@1.0.5: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.9 for-each: 0.3.5 gopd: 1.2.0 - has-proto: 1.2.0 is-typed-array: 1.1.15 reflect.getprototypeof: 1.0.10 - typed-array-length@1.0.7: + typed-array-length@1.0.8: dependencies: call-bind: 1.0.9 for-each: 0.3.5 @@ -11639,9 +9694,6 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript@5.8.2: - optional: true - typescript@6.0.3: {} ua-parser-js@1.0.41: {} @@ -11661,16 +9713,13 @@ snapshots: undici@8.11.2: {} - unicorn-magic@0.4.0: {} - - universalify@2.0.1: - optional: true + unicorn-magic@0.4.1: {} unpipe@1.0.0: {} - unplugin-dts@1.1.1(@microsoft/api-extractor@7.56.2(@types/node@26.6.3))(esbuild@0.28.2)(rolldown@1.2.1)(rollup@4.61.1)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)): + unplugin-dts@1.1.2(esbuild@0.28.2)(rolldown@1.2.12)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)): dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.61.1) + '@rollup/pluginutils': 5.4.0 '@volar/typescript': 2.4.28(typescript@6.0.3) compare-versions: 6.1.1 debug: 4.4.3(supports-color@10.2.2) @@ -11681,43 +9730,31 @@ snapshots: typescript: 6.0.3 unplugin: 2.3.11 optionalDependencies: - '@microsoft/api-extractor': 7.56.2(@types/node@26.6.3) esbuild: 0.28.2 - rolldown: 1.2.1 - rollup: 4.61.1 - vite: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) + rolldown: 1.2.12 + vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) transitivePeerDependencies: - supports-color unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 - acorn: 8.18.0 - picomatch: 4.0.5 + acorn: 8.19.0 + picomatch: 4.0.7 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.2.3(browserslist@4.28.7): + update-browserslist-db@1.3.3(browserslist@4.29.3): dependencies: - browserslist: 4.28.7 + browserslist: 4.29.3 escalade: 3.2.0 picocolors: 1.1.1 - update-browserslist-db@1.3.3(browserslist@4.29.2): - dependencies: - browserslist: 4.29.2 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - url@0.11.4: dependencies: punycode: 1.4.1 - qs: 6.15.3 + qs: 6.16.0 - use-sync-external-store@1.6.0(react@19.3.0): + use-sync-external-store@1.7.0(react@19.3.0): dependencies: react: 19.3.0 @@ -11733,7 +9770,7 @@ snapshots: is-arguments: 1.2.0 is-generator-function: 1.1.2 is-typed-array: 1.1.15 - which-typed-array: 1.1.22 + which-typed-array: 1.1.24 validate-npm-package-license@3.0.4: dependencies: @@ -11755,13 +9792,11 @@ snapshots: remove-trailing-separator: 1.1.0 replace-ext: 1.0.1 - vite-plugin-dts@5.1.1(@microsoft/api-extractor@7.56.2(@types/node@26.6.3))(esbuild@0.28.2)(rolldown@1.2.1)(rollup@4.61.1)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)): + vite-plugin-dts@5.1.2(esbuild@0.28.2)(rolldown@1.2.12)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)): dependencies: - unplugin-dts: 1.1.1(@microsoft/api-extractor@7.56.2(@types/node@26.6.3))(esbuild@0.28.2)(rolldown@1.2.1)(rollup@4.61.1)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) + unplugin-dts: 1.1.2(esbuild@0.28.2)(rolldown@1.2.12)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) optionalDependencies: - '@microsoft/api-extractor': 7.56.2(@types/node@26.6.3) - rollup: 4.61.1 - vite: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) + vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) transitivePeerDependencies: - '@rspack/core' - '@vue/language-core' @@ -11771,57 +9806,56 @@ snapshots: - typescript - webpack - vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0): + vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 - postcss: 8.5.28 - rolldown: 1.2.11 + postcss: 8.5.29 + rolldown: 1.2.12 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 esbuild: 0.28.2 fsevents: 2.3.3 - sass: 1.105.0 - sass-embedded: 1.105.0 + sass: 1.105.1 + sass-embedded: 1.105.1 - vitest@5.0.2(@types/node@26.6.3)(@vitest/browser-playwright@5.0.2)(@vitest/coverage-v8@5.0.2)(@vitest/ui@5.0.2)(jsdom@30.1.1(canvas@3.2.3))(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)): + vitest@5.0.3(@types/node@26.6.4)(@vitest/browser-playwright@5.0.3)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(jsdom@30.1.2(canvas@3.2.3))(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.2(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0)) - chai: 6.2.2 + '@vitest/mocker': 5.0.3(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1)) + chai: 6.3.0 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 1.4.2 - obug: 2.1.4 + magic-string: 1.4.3 + obug: 2.2.1 picomatch: 4.0.7 - std-env: 4.2.0 - tinybench: 6.2.0 + std-env: 4.3.0 + tinybench: 6.2.1 tinyexec: 1.3.1 tinyglobby: 0.2.17 - vite: 8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0) - why-is-node-running: 3.2.2 + vite: 8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1) + why-is-node-running: 3.2.1 optionalDependencies: - '@types/node': 26.6.3 - '@vitest/browser-playwright': 5.0.2(playwright@1.62.1)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(sass-embedded@1.105.0)(sass@1.105.0))(vitest@5.0.2) - '@vitest/coverage-v8': 5.0.2(@vitest/browser@5.0.2)(vitest@5.0.2) - '@vitest/ui': 5.0.2(vitest@5.0.2) - jsdom: 30.1.1(canvas@3.2.3) + '@types/node': 26.6.4 + '@vitest/browser-playwright': 5.0.3(playwright@1.62.1)(vite@8.3.3(@types/node@26.6.4)(esbuild@0.28.2)(sass-embedded@1.105.1)(sass@1.105.1))(vitest@5.0.3) + '@vitest/coverage-v8': 5.0.3(@vitest/browser@5.0.3)(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + jsdom: 30.1.2(canvas@3.2.3) transitivePeerDependencies: - msw - vscode-uri@3.1.0: {} + vscode-uri@3.2.0: {} w3c-xmlserializer@6.0.0: dependencies: xml-name-validator: 5.0.0 - wait-on@9.1.0(debug@4.4.3(supports-color@5.5.0))(supports-color@5.5.0): + wait-on@9.5.1(debug@4.4.3(supports-color@5.5.0))(supports-color@5.5.0): dependencies: - axios: 1.19.0(debug@4.4.3(supports-color@5.5.0))(supports-color@5.5.0) - joi: 18.2.3 + axios: 1.20.0(debug@4.4.3(supports-color@5.5.0))(supports-color@5.5.0) + joi: 18.2.9 lodash: 4.18.1 - minimist: 1.2.8 rxjs: 7.8.2 transitivePeerDependencies: - debug @@ -11841,18 +9875,10 @@ snapshots: whatwg-mimetype@5.0.0: {} - whatwg-url@16.0.1: + whatwg-url@17.2.0: dependencies: - '@exodus/bytes': 1.15.1 - tr46: 6.0.0 - webidl-conversions: 8.0.1 - transitivePeerDependencies: - - '@noble/hashes' - - whatwg-url@17.1.2: - dependencies: - '@exodus/bytes': 1.15.1 - tr46: 6.0.0 + '@exodus/bytes': 1.16.0 + tr46: 7.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: - '@noble/hashes' @@ -11873,7 +9899,7 @@ snapshots: which-builtin-type@1.2.1: dependencies: call-bound: 1.0.4 - function.prototype.name: 1.1.8 + function.prototype.name: 1.2.0 has-tostringtag: 1.0.2 is-async-function: 2.1.1 is-date-object: 1.1.0 @@ -11884,7 +9910,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.22 + which-typed-array: 1.1.24 which-collection@1.0.2: dependencies: @@ -11893,7 +9919,7 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.22: + which-typed-array@1.1.24: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.9 @@ -11907,11 +9933,7 @@ snapshots: dependencies: isexe: 2.0.0 - which@2.0.2: - dependencies: - isexe: 2.0.0 - - why-is-node-running@3.2.2: {} + why-is-node-running@3.2.1: {} winston-transport@4.9.0: dependencies: @@ -11921,8 +9943,8 @@ snapshots: winston@3.19.0: dependencies: - '@colors/colors': 1.6.0 - '@dabh/diagnostics': 2.0.8 + '@colors/colors': 1.6.1 + '@dabh/diagnostics': 2.0.9 async: 3.2.6 is-stream: 2.0.1 logform: 2.7.0 @@ -11933,8 +9955,6 @@ snapshots: triple-beam: 1.4.1 winston-transport: 4.9.0 - word-wrap@1.2.5: {} - workerpool@10.0.3: {} wrap-ansi@7.0.0: @@ -11955,8 +9975,6 @@ snapshots: dependencies: signal-exit: 4.1.0 - ws@8.21.2: {} - ws@8.22.0: {} wsl-utils@0.1.0: @@ -11977,14 +9995,11 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: - optional: true - yargs-parser@21.1.1: {} yargs-parser@22.0.0: {} - yargs@17.7.2: + yargs@17.7.3: dependencies: cliui: 8.0.1 escalade: 3.2.0 @@ -12007,12 +10022,4 @@ snapshots: dependencies: pend: 1.2.0 - yocto-queue@0.1.0: {} - yocto-queue@1.2.2: {} - - zod-validation-error@4.0.2(zod@4.3.6): - dependencies: - zod: 4.3.6 - - zod@4.3.6: {} diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index c30ce8c1ea..ad0a0c6f30 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -34,3 +34,7 @@ overrides: postcss@<8.5.10: ^8.5.10 yaml@>=2.0.0 <2.8.3: ^2.8.3 playwright@>=1.61.1 <2.0.0-0: "1.62.1" + postcss@<=8.5.11: ^8.5.12 + postcss@<=8.5.17: ^8.5.18 + postcss@<=8.5.22: ^8.5.23 + shell-quote@>=1.8.4 <1.11.0: ^1.11.0 diff --git a/library/package.json b/library/package.json index 7ce6b5fbf9..94da4f1912 100644 --- a/library/package.json +++ b/library/package.json @@ -34,7 +34,7 @@ "watch": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library" }, "devDependencies": { - "@types/node": "^26.1.2", + "@types/node": "^26.6.4", "@zip.js/zip.js": "2.8.34", "concurrently": "^10.0.5", "date-fns": "^4.4.0", diff --git a/library/pnpm-lock.yaml b/library/pnpm-lock.yaml index ec675f85ca..e7d3cc1f84 100644 --- a/library/pnpm-lock.yaml +++ b/library/pnpm-lock.yaml @@ -162,6 +162,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + shell-quote@>=1.8.4 <1.11.0: ^1.11.0 + patchedDependencies: '@zip.js/zip.js@2.8.34': 7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95 @@ -170,8 +173,8 @@ importers: .: devDependencies: '@types/node': - specifier: ^26.1.2 - version: 26.1.2 + specifier: ^26.6.4 + version: 26.6.4 '@zip.js/zip.js': specifier: 2.8.34 version: 2.8.34(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95) @@ -190,15 +193,15 @@ importers: packages: - '@types/node@26.1.2': - resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} + '@types/node@26.6.4': + resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} '@zip.js/zip.js@2.8.34': resolution: {integrity: sha512-+6a3lyqq69rpseLbvDPiVIWsZ/HdTGAAD6afFtug6ECPDGttb2dHnPC6cJgdPofYkzL9OvXizegq+DQVfL2rnA==} engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + ansi-regex@6.4.0: + resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==} engines: {node: '>=12'} ansi-styles@6.2.3: @@ -217,8 +220,8 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} braces@3.0.3: @@ -277,8 +280,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.6.0: - resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} engines: {node: '>=18'} glob-parent@5.1.2: @@ -308,8 +311,8 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} ms@2.1.3: @@ -343,8 +346,8 @@ packages: engines: {node: '>=10'} hasBin: true - shell-quote@1.9.0: - resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} + shell-quote@1.12.0: + resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==} engines: {node: '>= 0.4'} simple-update-notifier@2.0.0: @@ -392,8 +395,8 @@ packages: undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} @@ -413,13 +416,13 @@ packages: snapshots: - '@types/node@26.1.2': + '@types/node@26.6.4': dependencies: - undici-types: 8.3.0 + undici-types: 8.9.0 '@zip.js/zip.js@2.8.34(patch_hash=7b556bbd426f152eb086f0126a53900e369a95cf64357c380b7c8d8e940c3d95)': {} - ansi-regex@6.2.2: {} + ansi-regex@6.4.0: {} ansi-styles@6.2.3: {} @@ -432,7 +435,7 @@ snapshots: binary-extensions@2.3.0: {} - brace-expansion@5.0.9: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -466,7 +469,7 @@ snapshots: dependencies: chalk: 5.6.2 rxjs: 7.8.2 - shell-quote: 1.9.0 + shell-quote: 1.12.0 supports-color: 10.2.2 tree-kill: 1.2.2 yargs: 18.0.0 @@ -492,7 +495,7 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.6.0: {} + get-east-asian-width@1.7.0: {} glob-parent@5.1.2: dependencies: @@ -514,9 +517,9 @@ snapshots: is-number@7.0.0: {} - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 ms@2.1.3: {} @@ -525,7 +528,7 @@ snapshots: chokidar: 3.6.0 debug: 4.4.3(supports-color@5.5.0) ignore-by-default: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 pstree.remy: 1.1.8 semver: 7.8.5 simple-update-notifier: 2.0.0 @@ -549,7 +552,7 @@ snapshots: semver@7.8.5: {} - shell-quote@1.9.0: {} + shell-quote@1.12.0: {} simple-update-notifier@2.0.0: dependencies: @@ -565,12 +568,12 @@ snapshots: string-width@7.2.0: dependencies: emoji-regex: 10.6.0 - get-east-asian-width: 1.6.0 + get-east-asian-width: 1.7.0 strip-ansi: 7.2.0 strip-ansi@7.2.0: dependencies: - ansi-regex: 6.2.2 + ansi-regex: 6.4.0 supports-color@10.2.2: {} @@ -590,7 +593,7 @@ snapshots: undefsafe@2.0.5: {} - undici-types@8.3.0: {} + undici-types@8.9.0: {} wrap-ansi@9.0.2: dependencies: diff --git a/library/pnpm-workspace.yaml b/library/pnpm-workspace.yaml index 528f811e1a..75d97089bc 100644 --- a/library/pnpm-workspace.yaml +++ b/library/pnpm-workspace.yaml @@ -4,3 +4,5 @@ minimumReleaseAgeExclude: - brace-expansion@5.0.8 || 5.0.9 patchedDependencies: '@zip.js/zip.js@2.8.34': patches/@zip.js__zip.js@2.8.11.patch +overrides: + shell-quote@>=1.8.4 <1.11.0: ^1.11.0 diff --git a/mcp/packages/common/package.json b/mcp/packages/common/package.json index c8bdde4bd0..ed573b019a 100644 --- a/mcp/packages/common/package.json +++ b/mcp/packages/common/package.json @@ -12,7 +12,7 @@ "clean": "rm -rf dist/" }, "devDependencies": { - "typescript": "^6.0.3" + "typescript": "^7.0.2" }, "files": [ "dist/**/*" diff --git a/mcp/packages/plugin/package.json b/mcp/packages/plugin/package.json index f4b5e9d68c..782150744f 100644 --- a/mcp/packages/plugin/package.json +++ b/mcp/packages/plugin/package.json @@ -18,8 +18,8 @@ }, "devDependencies": { "cross-env": "^10.1.0", - "typescript": "^6.0.3", - "vite": "^8.3.1", + "typescript": "^7.0.2", + "vite": "^8.3.2", "vite-live-preview": "^0.4.0" } } diff --git a/mcp/packages/server/package.json b/mcp/packages/server/package.json index 455b2784ba..124ab8b60a 100644 --- a/mcp/packages/server/package.json +++ b/mcp/packages/server/package.json @@ -29,34 +29,34 @@ }, "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec", "dependencies": { - "@modelcontextprotocol/node": "^2.1.0", - "@modelcontextprotocol/server": "^2.1.0", + "@modelcontextprotocol/node": "^2.1.1", + "@modelcontextprotocol/server": "^2.3.1", "class-transformer": "^0.5.1", "class-validator": "^0.15.1", "express": "^5.2.1", "ioredis": "^6.0.0", "js-yaml": "^5.4.2", "nrepl-client": "^0.3.0", - "pino": "^10.3.1", + "pino": "^10.4.0", "pino-loki": "^3.0.0", - "pino-pretty": "^13.1.3", + "pino-pretty": "^13.2.0", "reflect-metadata": "^0.2.2", "sharp": "^0.35.5", "ws": "^8.22.0", "zod": "^4.6.5" }, "devDependencies": { - "@modelcontextprotocol/client": "^2.1.0", + "@modelcontextprotocol/client": "^2.3.1", "@penpot/mcp-common": "workspace:../common", "@types/express": "^5.0.6", "@types/js-yaml": "^4.0.9", - "@types/node": "^26.6.3", - "@types/ws": "^8.18.1", + "@types/node": "^26.6.4", + "@types/ws": "^8.18.2", "cross-env": "^10.1.0", "esbuild": "^0.28.1", "ts-node": "^10.9.2", "tsx": "^4.23.15", - "typescript": "^6.0.3" + "typescript": "^7.0.2" }, "ts-node": { "esm": true diff --git a/mcp/pnpm-lock.yaml b/mcp/pnpm-lock.yaml index 00545675bb..053f1f6135 100644 --- a/mcp/pnpm-lock.yaml +++ b/mcp/pnpm-lock.yaml @@ -164,6 +164,7 @@ settings: overrides: '@hono/node-server@<2.0.5': ^2.0.5 + shell-quote@>=1.8.4 <1.11.0: ^1.11.0 importers: @@ -179,8 +180,8 @@ importers: packages/common: devDependencies: typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^7.0.2 + version: 7.0.2 packages/plugin: dependencies: @@ -195,23 +196,23 @@ importers: specifier: ^10.1.0 version: 10.1.0 typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^7.0.2 + version: 7.0.2 vite: - specifier: ^8.3.1 - version: 8.3.1(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.12) + specifier: ^8.3.2 + version: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(tsx@4.23.15) vite-live-preview: specifier: ^0.4.0 - version: 0.4.0(vite@8.3.1(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.12)) + version: 0.4.0(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(tsx@4.23.15)) packages/server: dependencies: '@modelcontextprotocol/node': - specifier: ^2.1.0 - version: 2.1.0(@modelcontextprotocol/server@2.1.0)(hono@4.13.5) + specifier: ^2.1.1 + version: 2.1.1(@modelcontextprotocol/server@2.3.1) '@modelcontextprotocol/server': - specifier: ^2.1.0 - version: 2.1.0 + specifier: ^2.3.1 + version: 2.3.1 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -231,20 +232,20 @@ importers: specifier: ^0.3.0 version: 0.3.0 pino: - specifier: ^10.3.1 - version: 10.3.1 + specifier: ^10.4.0 + version: 10.4.0 pino-loki: specifier: ^3.0.0 version: 3.0.0 pino-pretty: - specifier: ^13.1.3 - version: 13.1.3 + specifier: ^13.2.0 + version: 13.2.0 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 sharp: specifier: ^0.35.5 - version: 0.35.5(@types/node@26.6.3) + version: 0.35.5(@types/node@26.6.4) ws: specifier: ^8.22.0 version: 8.22.0 @@ -253,8 +254,8 @@ importers: version: 4.6.5 devDependencies: '@modelcontextprotocol/client': - specifier: ^2.1.0 - version: 2.1.0 + specifier: ^2.3.1 + version: 2.3.1 '@penpot/mcp-common': specifier: workspace:../common version: link:../common @@ -265,11 +266,11 @@ importers: specifier: ^4.0.9 version: 4.0.9 '@types/node': - specifier: ^26.6.3 - version: 26.6.3 + specifier: ^26.6.4 + version: 26.6.4 '@types/ws': - specifier: ^8.18.1 - version: 8.18.1 + specifier: ^8.18.2 + version: 8.18.2 cross-env: specifier: ^10.1.0 version: 10.1.0 @@ -278,13 +279,13 @@ importers: version: 0.28.2 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@26.6.3)(typescript@6.0.3) + version: 10.9.2(@types/node@26.6.4)(typescript@7.0.2) tsx: specifier: ^4.23.15 version: 4.23.15 typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^7.0.2 + version: 7.0.2 packages: @@ -454,8 +455,8 @@ packages: cpu: [x64] os: [win32] - '@hono/node-server@2.1.1': - resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==} + '@hono/node-server@2.1.3': + resolution: {integrity: sha512-TA//nWMqPhbfdfneACk6t5a9eqbS9lABEPyKn0/xZTah3H3U2XaVg85rJFl0/Fyit0I552YDHgXGVSf3GwqbUw==} engines: {node: '>=20'} peerDependencies: hono: ^4 @@ -629,36 +630,32 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@modelcontextprotocol/client@2.1.0': - resolution: {integrity: sha512-mDVhoy5WjDb0U+4dQPzLcciC2erSex/GRVQqnoZdiuMoE3YiTZdiS7ezNcFwX4XEOWOaj7jZr0kLYnILnL8orA==} + '@modelcontextprotocol/client@2.3.1': + resolution: {integrity: sha512-mIGZXpHsjnZ6lD+gD/WCMpR5k8yVQQ8nNFH1N0Srf7AvnwTUMYD6pvTY8ng2+He5FfV7YMZLmrjL7cLa/cc3dQ==} engines: {node: '>=20'} - '@modelcontextprotocol/core@2.1.0': - resolution: {integrity: sha512-YdFj5gMRHr0gYNAhTbQDgjLHiEJdIAUYxomhKseUNye7ZGlISQmUoLh2sveFFCiQ7j2YMffT1kgSND0FSfIFjQ==} + '@modelcontextprotocol/core@2.3.1': + resolution: {integrity: sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g==} engines: {node: '>=20'} - '@modelcontextprotocol/node@2.1.0': - resolution: {integrity: sha512-6xg3iWVcOfiL8Y7rI6xUqXD0lI2AY5q3djsa/cOi1IJUSwx06t0gvhSwU/6mIIohrFlt40/ANUG56DQ0HLhILQ==} + '@modelcontextprotocol/node@2.1.1': + resolution: {integrity: sha512-EY92OAXN2xYL7tNS/MVsWJW1v8VHAPwl9B4Ca3pCrjmvqyC+S1C/6UHsMxF2GoFPraS5Yyg56vFz9brVpFojMA==} engines: {node: '>=20'} peerDependencies: - '@modelcontextprotocol/server': ^2.1.0 - hono: ^4.11.4 - peerDependenciesMeta: - hono: - optional: true + '@modelcontextprotocol/server': ^2.3.0 - '@modelcontextprotocol/server@2.1.0': - resolution: {integrity: sha512-FdvwZU5N03O77Suh35JqjAqq5R1ZTEPFtwu6N4BgBq28qrQtrCnpbLL5U2pwYCuE194jAw0xnlOVmIqhfoYuUQ==} + '@modelcontextprotocol/server@2.3.1': + resolution: {integrity: sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw==} engines: {node: '>=20'} - '@oxc-project/types@0.151.0': - resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==} + '@oxc-project/types@0.152.0': + resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} '@penpot/plugin-styles@1.5.0': resolution: {integrity: sha512-rwXFBRPd0IZs3ens+eRT8oA5WqkQgF6xlW6fCfH7U1NHRjej9HH7vTGXmfjoIVml6Xx1sbyYHZVxkQC8Ub8+zA==} @@ -669,98 +666,98 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - '@rolldown/binding-android-arm-eabi@1.2.11': - resolution: {integrity: sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==} + '@rolldown/binding-android-arm-eabi@1.2.12': + resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rolldown/binding-android-arm64@1.2.11': - resolution: {integrity: sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==} + '@rolldown/binding-android-arm64@1.2.12': + resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.11': - resolution: {integrity: sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==} + '@rolldown/binding-darwin-arm64@1.2.12': + resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.11': - resolution: {integrity: sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==} + '@rolldown/binding-darwin-x64@1.2.12': + resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.11': - resolution: {integrity: sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==} + '@rolldown/binding-freebsd-x64@1.2.12': + resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.11': - resolution: {integrity: sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': + resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.11': - resolution: {integrity: sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==} + '@rolldown/binding-linux-arm64-gnu@1.2.12': + resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.11': - resolution: {integrity: sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==} + '@rolldown/binding-linux-arm64-musl@1.2.12': + resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.11': - resolution: {integrity: sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==} + '@rolldown/binding-linux-ppc64-gnu@1.2.12': + resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.11': - resolution: {integrity: sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==} + '@rolldown/binding-linux-s390x-gnu@1.2.12': + resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.11': - resolution: {integrity: sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==} + '@rolldown/binding-linux-x64-gnu@1.2.12': + resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.11': - resolution: {integrity: sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==} + '@rolldown/binding-linux-x64-musl@1.2.12': + resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.11': - resolution: {integrity: sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==} + '@rolldown/binding-openharmony-arm64@1.2.12': + resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.11': - resolution: {integrity: sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==} + '@rolldown/binding-win32-arm64-msvc@1.2.12': + resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.11': - resolution: {integrity: sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==} + '@rolldown/binding-win32-x64-msvc@1.2.12': + resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -804,11 +801,8 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - '@types/node@26.4.0': - resolution: {integrity: sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==} - - '@types/node@26.6.3': - resolution: {integrity: sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==} + '@types/node@26.6.4': + resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} '@types/qs@6.15.1': resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} @@ -825,8 +819,128 @@ packages: '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/ws@8.18.2': + resolution: {integrity: sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -836,13 +950,13 @@ packages: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} - acorn@8.18.0: - resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + acorn@8.19.0: + resolution: {integrity: sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==} engines: {node: '>=0.4.0'} hasBin: true - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + ansi-regex@6.4.0: + resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==} engines: {node: '>=12'} ansi-styles@6.2.3: @@ -1025,8 +1139,8 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - fast-copy@4.0.4: - resolution: {integrity: sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==} + fast-copy@4.1.2: + resolution: {integrity: sha512-DUHggaYcouhy5I1LacDJ+JDSqGlumVJP2EAi5Nwt1pWPeLpSvIjkrY+x4fsoI1/+GlTyq+DM9zdgWCFSfqNODA==} fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} @@ -1064,8 +1178,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.6.0: - resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} engines: {node: '>=18'} get-intrinsic@1.3.0: @@ -1091,8 +1205,8 @@ packages: help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} - hono@4.13.5: - resolution: {integrity: sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==} + hono@4.13.13: + resolution: {integrity: sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==} engines: {node: '>=16.9.0'} http-errors@2.0.1: @@ -1120,8 +1234,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jose@6.2.10: - resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} + jose@6.2.12: + resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -1131,8 +1245,8 @@ packages: resolution: {integrity: sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==} hasBin: true - libphonenumber-js@1.13.11: - resolution: {integrity: sha512-ETER2kMaIFTI/Nh1a8Gk03dUF/SL0VZqtI+CcVHZxp5WIHYwNS7S+uiYZDYCvLy3lOR4/DAD5jf0h5WkePPpqg==} + libphonenumber-js@1.13.14: + resolution: {integrity: sha512-llihgCcx0BFLksecLP+x1J+6JDE1GsXS1RN/LoPF6qcwpeQcnjj0lcvZxY8AzbEpYwyZWPZW/nDuqkqzm3amiw==} lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} @@ -1237,8 +1351,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1290,23 +1404,23 @@ packages: engines: {node: '>=20'} hasBin: true - pino-pretty@13.1.3: - resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + pino-pretty@13.2.0: + resolution: {integrity: sha512-jjghD1ML+Di/WxjJDz6j6Y4FT7hIXOlb8h27AyJ+ohtAXJXH4PS/xkv7si+RCCIW59HMgZsMKTDgzdr0RhuRrg==} hasBin: true pino-std-serializers@7.1.0: resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} - pino@10.3.1: - resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + pino@10.4.0: + resolution: {integrity: sha512-bk1ZMTwG/Vymx+zPoa28MhNzucFvXJWp3csrSuxuHPSMeUettaCoQ5Jvg07mtC4JGNpcfTwz41oVCnNgBK9+vA==} hasBin: true pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} - postcss@8.5.28: - resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + postcss@8.5.29: + resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==} engines: {node: ^10 || ^12 || >=14} prettier@3.9.9: @@ -1317,15 +1431,15 @@ packages: process-warning@5.1.0: resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} quick-format-unescaped@4.0.4: @@ -1339,10 +1453,6 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} - real-require@0.2.0: - resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} - engines: {node: '>= 12.13.0'} - real-require@1.0.0: resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} @@ -1353,8 +1463,8 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - rolldown@1.2.11: - resolution: {integrity: sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==} + rolldown@1.2.12: + resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -1408,8 +1518,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.9.0: - resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} + shell-quote@1.12.0: + resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==} engines: {node: '>= 0.4'} side-channel-list@1.0.1: @@ -1431,8 +1541,11 @@ packages: sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + sonic-boom@5.0.1: + resolution: {integrity: sha512-I1E9tHuKe6SdS/Uab4bl2o1ZUBm2SCTDFF9eQ66ehyVcFIBixfecy9DlwXCtnRF8CUzPw7Q4Dn6JQE/8iVl/FQ==} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} split2@4.2.0: @@ -1495,11 +1608,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.23.12: - resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} - engines: {node: '>=18.0.0'} - hasBin: true - tsx@4.23.15: resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} engines: {node: '>=18.0.0'} @@ -1509,14 +1617,11 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} hasBin: true - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - undici-types@8.9.0: resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} @@ -1540,8 +1645,8 @@ packages: peerDependencies: vite: '>=5.4.0' - vite@8.3.1: - resolution: {integrity: sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==} + vite@8.3.2: + resolution: {integrity: sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -1595,18 +1700,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.21.3: - resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.22.0: resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} engines: {node: '>=10.0.0'} @@ -1729,9 +1822,9 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@hono/node-server@2.1.1(hono@4.13.5)': + '@hono/node-server@2.1.3(hono@4.13.13)': dependencies: - hono: 4.13.5 + hono: 4.13.13 '@img/colour@1.1.0': {} @@ -1843,40 +1936,39 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 - '@modelcontextprotocol/client@2.1.0': + '@modelcontextprotocol/client@2.3.1': dependencies: - '@modelcontextprotocol/core': 2.1.0 + '@modelcontextprotocol/core': 2.3.1 cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.1.1 - jose: 6.2.10 + jose: 6.2.12 pkce-challenge: 5.0.1 zod: 4.6.5 - '@modelcontextprotocol/core@2.1.0': + '@modelcontextprotocol/core@2.3.1': dependencies: zod: 4.6.5 - '@modelcontextprotocol/node@2.1.0(@modelcontextprotocol/server@2.1.0)(hono@4.13.5)': + '@modelcontextprotocol/node@2.1.1(@modelcontextprotocol/server@2.3.1)': dependencies: - '@hono/node-server': 2.1.1(hono@4.13.5) - '@modelcontextprotocol/server': 2.1.0 - optionalDependencies: - hono: 4.13.5 + '@hono/node-server': 2.1.3(hono@4.13.13) + '@modelcontextprotocol/server': 2.3.1 + hono: 4.13.13 - '@modelcontextprotocol/server@2.1.0': + '@modelcontextprotocol/server@2.3.1': dependencies: - '@modelcontextprotocol/core': 2.1.0 + '@modelcontextprotocol/core': 2.3.1 zod: 4.6.5 - '@oxc-project/types@0.151.0': {} + '@oxc-project/types@0.152.0': {} '@penpot/plugin-styles@1.5.0': {} @@ -1884,49 +1976,49 @@ snapshots: '@pinojs/redact@0.4.0': {} - '@rolldown/binding-android-arm-eabi@1.2.11': + '@rolldown/binding-android-arm-eabi@1.2.12': optional: true - '@rolldown/binding-android-arm64@1.2.11': + '@rolldown/binding-android-arm64@1.2.12': optional: true - '@rolldown/binding-darwin-arm64@1.2.11': + '@rolldown/binding-darwin-arm64@1.2.12': optional: true - '@rolldown/binding-darwin-x64@1.2.11': + '@rolldown/binding-darwin-x64@1.2.12': optional: true - '@rolldown/binding-freebsd-x64@1.2.11': + '@rolldown/binding-freebsd-x64@1.2.12': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.11': + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.11': + '@rolldown/binding-linux-arm64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.11': + '@rolldown/binding-linux-arm64-musl@1.2.12': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.11': + '@rolldown/binding-linux-ppc64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.11': + '@rolldown/binding-linux-s390x-gnu@1.2.12': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.11': + '@rolldown/binding-linux-x64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-x64-musl@1.2.11': + '@rolldown/binding-linux-x64-musl@1.2.12': optional: true - '@rolldown/binding-openharmony-arm64@1.2.11': + '@rolldown/binding-openharmony-arm64@1.2.12': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.11': + '@rolldown/binding-win32-arm64-msvc@1.2.12': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.11': + '@rolldown/binding-win32-x64-msvc@1.2.12': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -1946,15 +2038,15 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 26.6.3 + '@types/node': 26.6.4 '@types/connect@3.4.38': dependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 '@types/express-serve-static-core@5.1.3': dependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 '@types/qs': 6.15.1 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -1969,11 +2061,7 @@ snapshots: '@types/js-yaml@4.0.9': {} - '@types/node@26.4.0': - dependencies: - undici-types: 8.3.0 - - '@types/node@26.6.3': + '@types/node@26.6.4': dependencies: undici-types: 8.9.0 @@ -1983,18 +2071,78 @@ snapshots: '@types/send@1.2.1': dependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 26.6.3 + '@types/node': 26.6.4 '@types/validator@13.15.10': {} - '@types/ws@8.18.1': + '@types/ws@8.18.2': dependencies: - '@types/node': 26.4.0 + '@types/node': 26.6.4 + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true accepts@2.0.0: dependencies: @@ -2003,11 +2151,11 @@ snapshots: acorn-walk@8.3.5: dependencies: - acorn: 8.18.0 + acorn: 8.19.0 - acorn@8.18.0: {} + acorn@8.19.0: {} - ansi-regex@6.2.2: {} + ansi-regex@6.4.0: {} ansi-styles@6.2.3: {} @@ -2027,7 +2175,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.15.3 + qs: 6.16.0 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -2052,7 +2200,7 @@ snapshots: class-validator@0.15.1: dependencies: '@types/validator': 13.15.10 - libphonenumber-js: 1.13.11 + libphonenumber-js: 1.13.14 validator: 13.15.35 cliui@9.0.1: @@ -2069,7 +2217,7 @@ snapshots: dependencies: chalk: 5.6.2 rxjs: 7.8.2 - shell-quote: 1.9.0 + shell-quote: 1.12.0 supports-color: 10.2.2 tree-kill: 1.2.2 yargs: 18.0.0 @@ -2201,8 +2349,8 @@ snapshots: on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.3 + proxy-addr: 2.0.8 + qs: 6.16.0 range-parser: 1.3.0 router: 2.2.0(supports-color@10.2.2) send: 1.2.1(supports-color@10.2.2) @@ -2213,7 +2361,7 @@ snapshots: transitivePeerDependencies: - supports-color - fast-copy@4.0.4: {} + fast-copy@4.1.2: {} fast-safe-stringify@2.1.1: {} @@ -2243,7 +2391,7 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.6.0: {} + get-east-asian-width@1.7.0: {} get-intrinsic@1.3.0: dependencies: @@ -2273,7 +2421,7 @@ snapshots: help-me@5.0.0: {} - hono@4.13.5: {} + hono@4.13.13: {} http-errors@2.0.1: dependencies: @@ -2306,7 +2454,7 @@ snapshots: isexe@2.0.0: {} - jose@6.2.10: {} + jose@6.2.12: {} joycon@3.1.1: {} @@ -2314,7 +2462,7 @@ snapshots: dependencies: argparse: 2.0.1 - libphonenumber-js@1.13.11: {} + libphonenumber-js@1.13.14: {} lightningcss-android-arm64@1.33.0: optional: true @@ -2383,7 +2531,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.18: {} + nanoid@3.3.20: {} negotiator@1.1.0: dependencies: @@ -2425,11 +2573,11 @@ snapshots: pino-abstract-transport: 3.0.0 pump: 3.0.4 - pino-pretty@13.1.3: + pino-pretty@13.2.0: dependencies: colorette: 2.0.20 dateformat: 4.6.3 - fast-copy: 4.0.4 + fast-copy: 4.1.2 fast-safe-stringify: 2.1.1 help-me: 5.0.0 joycon: 3.1.1 @@ -2438,12 +2586,12 @@ snapshots: pino-abstract-transport: 3.0.0 pump: 3.0.4 secure-json-parse: 4.1.0 - sonic-boom: 4.2.1 + sonic-boom: 5.0.1 strip-json-comments: 5.0.3 pino-std-serializers@7.1.0: {} - pino@10.3.1: + pino@10.4.0: dependencies: '@pinojs/redact': 0.4.0 atomic-sleep: 1.0.0 @@ -2452,24 +2600,24 @@ snapshots: pino-std-serializers: 7.1.0 process-warning: 5.1.0 quick-format-unescaped: 4.0.4 - real-require: 0.2.0 + real-require: 1.0.0 safe-stable-stringify: 2.5.0 sonic-boom: 4.2.1 thread-stream: 4.2.0 pkce-challenge@5.0.1: {} - postcss@8.5.28: + postcss@8.5.29: dependencies: - nanoid: 3.3.18 + nanoid: 3.3.20 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 prettier@3.9.9: {} process-warning@5.1.0: {} - proxy-addr@2.0.7: + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 @@ -2479,7 +2627,7 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 - qs@6.15.3: + qs@6.16.0: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 @@ -2495,34 +2643,32 @@ snapshots: iconv-lite: 0.7.3 unpipe: 1.0.0 - real-require@0.2.0: {} - real-require@1.0.0: {} redis-errors@1.2.0: {} reflect-metadata@0.2.2: {} - rolldown@1.2.11: + rolldown@1.2.12: dependencies: - '@oxc-project/types': 0.151.0 + '@oxc-project/types': 0.152.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.11 - '@rolldown/binding-android-arm64': 1.2.11 - '@rolldown/binding-darwin-arm64': 1.2.11 - '@rolldown/binding-darwin-x64': 1.2.11 - '@rolldown/binding-freebsd-x64': 1.2.11 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.11 - '@rolldown/binding-linux-arm64-gnu': 1.2.11 - '@rolldown/binding-linux-arm64-musl': 1.2.11 - '@rolldown/binding-linux-ppc64-gnu': 1.2.11 - '@rolldown/binding-linux-s390x-gnu': 1.2.11 - '@rolldown/binding-linux-x64-gnu': 1.2.11 - '@rolldown/binding-linux-x64-musl': 1.2.11 - '@rolldown/binding-openharmony-arm64': 1.2.11 - '@rolldown/binding-win32-arm64-msvc': 1.2.11 - '@rolldown/binding-win32-x64-msvc': 1.2.11 + '@rolldown/binding-android-arm-eabi': 1.2.12 + '@rolldown/binding-android-arm64': 1.2.12 + '@rolldown/binding-darwin-arm64': 1.2.12 + '@rolldown/binding-darwin-x64': 1.2.12 + '@rolldown/binding-freebsd-x64': 1.2.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.12 + '@rolldown/binding-linux-arm64-gnu': 1.2.12 + '@rolldown/binding-linux-arm64-musl': 1.2.12 + '@rolldown/binding-linux-ppc64-gnu': 1.2.12 + '@rolldown/binding-linux-s390x-gnu': 1.2.12 + '@rolldown/binding-linux-x64-gnu': 1.2.12 + '@rolldown/binding-linux-x64-musl': 1.2.12 + '@rolldown/binding-openharmony-arm64': 1.2.12 + '@rolldown/binding-win32-arm64-msvc': 1.2.12 + '@rolldown/binding-win32-x64-msvc': 1.2.12 router@2.2.0(supports-color@10.2.2): dependencies: @@ -2573,7 +2719,7 @@ snapshots: setprototypeof@1.2.0: {} - sharp@0.35.5(@types/node@26.6.3): + sharp@0.35.5(@types/node@26.6.4): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 @@ -2604,7 +2750,7 @@ snapshots: '@img/sharp-win32-arm64': 0.35.5 '@img/sharp-win32-ia32': 0.35.5 '@img/sharp-win32-x64': 0.35.5 - '@types/node': 26.6.3 + '@types/node': 26.6.4 shebang-command@2.0.0: dependencies: @@ -2612,7 +2758,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.9.0: {} + shell-quote@1.12.0: {} side-channel-list@1.0.1: dependencies: @@ -2646,7 +2792,11 @@ snapshots: dependencies: atomic-sleep: 1.0.0 - source-map-js@1.2.1: {} + sonic-boom@5.0.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.2: {} split2@4.2.0: {} @@ -2657,12 +2807,12 @@ snapshots: string-width@7.2.0: dependencies: emoji-regex: 10.6.0 - get-east-asian-width: 1.6.0 + get-east-asian-width: 1.7.0 strip-ansi: 7.2.0 strip-ansi@7.2.0: dependencies: - ansi-regex: 6.2.2 + ansi-regex: 6.4.0 strip-json-comments@5.0.3: {} @@ -2681,33 +2831,26 @@ snapshots: tree-kill@1.2.2: {} - ts-node@10.9.2(@types/node@26.6.3)(typescript@6.0.3): + ts-node@10.9.2(@types/node@26.6.4)(typescript@7.0.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.13 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 26.6.3 - acorn: 8.18.0 + '@types/node': 26.6.4 + acorn: 8.19.0 acorn-walk: 8.3.5 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.4 make-error: 1.3.6 - typescript: 6.0.3 + typescript: 7.0.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 tslib@2.8.1: {} - tsx@4.23.12: - dependencies: - esbuild: 0.28.2 - optionalDependencies: - fsevents: 2.3.3 - optional: true - tsx@4.23.15: dependencies: esbuild: 0.28.2 @@ -2720,9 +2863,28 @@ snapshots: media-typer: 1.1.1 mime-types: 3.0.2 - typescript@6.0.3: {} - - undici-types@8.3.0: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 undici-types@8.9.0: {} @@ -2734,31 +2896,31 @@ snapshots: vary@1.1.2: {} - vite-live-preview@0.4.0(vite@8.3.1(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.12)): + vite-live-preview@0.4.0(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(tsx@4.23.15)): dependencies: '@seahax/deep-copy': 0.1.0 '@seahax/semaphore': 0.5.1 - '@types/ws': 8.18.1 + '@types/ws': 8.18.2 escape-goat: 4.0.0 strip-ansi: 7.2.0 - vite: 8.3.1(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.12) - ws: 8.21.3 + vite: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(tsx@4.23.15) + ws: 8.22.0 transitivePeerDependencies: - bufferutil - utf-8-validate - vite@8.3.1(@types/node@26.4.0)(esbuild@0.28.2)(tsx@4.23.12): + vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(tsx@4.23.15): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 - postcss: 8.5.28 - rolldown: 1.2.11 + postcss: 8.5.29 + rolldown: 1.2.12 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.4.0 + '@types/node': 26.6.4 esbuild: 0.28.2 fsevents: 2.3.3 - tsx: 4.23.12 + tsx: 4.23.15 which@2.0.2: dependencies: @@ -2772,8 +2934,6 @@ snapshots: wrappy@1.0.2: {} - ws@8.21.3: {} - ws@8.22.0: {} y18n@5.0.8: {} diff --git a/mcp/pnpm-workspace.yaml b/mcp/pnpm-workspace.yaml index 13e111b1dc..3da100dca6 100644 --- a/mcp/pnpm-workspace.yaml +++ b/mcp/pnpm-workspace.yaml @@ -22,3 +22,4 @@ packages: overrides: '@hono/node-server@<2.0.5': ^2.0.5 + shell-quote@>=1.8.4 <1.11.0: ^1.11.0 diff --git a/plugins/apps/composable-test-suite/package.json b/plugins/apps/composable-test-suite/package.json index 20aae69b6c..feee72036d 100644 --- a/plugins/apps/composable-test-suite/package.json +++ b/plugins/apps/composable-test-suite/package.json @@ -21,10 +21,10 @@ "@penpot/plugin-types": "1.4.2" }, "devDependencies": { - "playwright": "^1.62.1", - "prettier": "^3.9.6", - "typescript": "^5.9.3", - "vite": "^8.2.2", + "playwright": "^1.63.0", + "prettier": "^3.9.9", + "typescript": "^7.0.2", + "vite": "^8.3.2", "vite-live-preview": "^0.4.0" }, "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" diff --git a/plugins/apps/composable-test-suite/src/vite-env.d.ts b/plugins/apps/composable-test-suite/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/plugins/apps/composable-test-suite/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/plugins/apps/plugin-api-test-suite/package.json b/plugins/apps/plugin-api-test-suite/package.json index 28f81cac97..7bbf13f2df 100644 --- a/plugins/apps/plugin-api-test-suite/package.json +++ b/plugins/apps/plugin-api-test-suite/package.json @@ -18,7 +18,7 @@ "test:unit": "tsx --test ci/static-server.test.ts" }, "devDependencies": { - "playwright": "^1.62.1" + "playwright": "^1.63.0" }, "packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec" } diff --git a/plugins/package.json b/plugins/package.json index 60354ab878..2f6630b530 100644 --- a/plugins/package.json +++ b/plugins/package.json @@ -32,60 +32,60 @@ }, "private": true, "devDependencies": { - "@angular-devkit/core": "22.2.0", - "@angular-devkit/schematics": "22.2.0", + "@angular-devkit/core": "22.2.1", + "@angular-devkit/schematics": "22.2.1", "@angular-eslint/eslint-plugin": "22.5.0", "@angular-eslint/eslint-plugin-template": "22.5.0", "@angular-eslint/template-parser": "22.5.0", - "@angular/build": "22.2.0", - "@angular/cli": "22.2.0", - "@angular/compiler-cli": "22.2.0", - "@angular/language-service": "22.2.0", + "@angular/build": "22.2.1", + "@angular/cli": "22.2.1", + "@angular/compiler-cli": "22.2.1", + "@angular/language-service": "22.2.1", "@eslint/js": "10.0.1", - "@schematics/angular": "22.2.0", + "@schematics/angular": "22.2.1", "@types/feather-icons": "^4.29.4", - "@types/node": "26.6.3", + "@types/node": "26.6.4", "@types/yargs": "^17.0.35", - "@typescript-eslint/eslint-plugin": "8.70.1", - "@typescript-eslint/parser": "8.70.1", - "@typescript-eslint/utils": "^8.68.0", - "@vitest/coverage-v8": "4.1.11", - "@vitest/ui": "4.1.11", + "@typescript-eslint/eslint-plugin": "8.71.1", + "@typescript-eslint/parser": "8.71.1", + "@typescript-eslint/utils": "^8.71.1", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", "concurrently": "^10.0.5", - "dotenv": "^17.4.2", + "dotenv": "^18.0.5", "esbuild": "^0.28.2", - "eslint": "10.9.1", + "eslint": "10.12.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-unused-imports": "^4.4.1", - "fs-extra": "^11.4.0", - "globals": "^17.11.0", - "happy-dom": "^20.11.6", + "fs-extra": "^11.4.1", + "globals": "^17.13.0", + "happy-dom": "^20.14.5", "jiti": "2.7.0", - "jsdom": "~30.0.1", + "jsdom": "~30.1.2", "jsonc-eslint-parser": "^3.3.0", - "prettier": "^3.9.6", - "tsx": "^4.23.12", + "prettier": "^3.9.9", + "tsx": "^4.23.15", "typedoc": "^0.28.20", - "typescript": "6.0.3", - "typescript-eslint": "^8.68.0", - "vite": "8.2.2", + "typescript": "6.0.2", + "typescript-eslint": "^8.71.1", + "vite": "8.3.2", "vite-plugin-checker": "^0.14.5", - "vite-plugin-dts": "5.0.3", + "vite-plugin-dts": "5.1.2", "vite-plugin-static-copy": "^4.1.1", - "vitest": "4.1.11", - "yargs": "^18.1.0" + "vitest": "5.0.3", + "yargs": "^18.2.0" }, "dependencies": { - "@angular/common": "22.2.0", - "@angular/compiler": "22.2.0", - "@angular/core": "22.2.0", - "@angular/forms": "22.2.0", - "@angular/platform-browser": "22.2.0", - "@angular/router": "22.2.0", + "@angular/common": "22.2.1", + "@angular/compiler": "22.2.1", + "@angular/core": "22.2.1", + "@angular/forms": "22.2.1", + "@angular/platform-browser": "22.2.1", + "@angular/router": "22.2.1", "axios": "^1.20.0", "feather-icons": "^4.29.2", "puppeteer": "^25.12.0", diff --git a/plugins/pnpm-lock.yaml b/plugins/pnpm-lock.yaml index 470b8c5bba..02dda4ee4d 100644 --- a/plugins/pnpm-lock.yaml +++ b/plugins/pnpm-lock.yaml @@ -177,23 +177,23 @@ importers: .: dependencies: '@angular/common': - specifier: 22.2.0 - version: 22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + specifier: 22.2.1 + version: 22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) '@angular/compiler': - specifier: 22.2.0 - version: 22.2.0 + specifier: 22.2.1 + version: 22.2.1 '@angular/core': - specifier: 22.2.0 - version: 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3) + specifier: 22.2.1 + version: 22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3) '@angular/forms': - specifier: 22.2.0 - version: 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) + specifier: 22.2.1 + version: 22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) '@angular/platform-browser': - specifier: 22.2.0 - version: 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)) + specifier: 22.2.1 + version: 22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)) '@angular/router': - specifier: 22.2.0 - version: 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) + specifier: 22.2.1 + version: 22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2) axios: specifier: ^1.20.0 version: 1.20.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) @@ -220,143 +220,143 @@ importers: version: 0.16.3 devDependencies: '@angular-devkit/core': - specifier: 22.2.0 - version: 22.2.0(chokidar@5.0.0) + specifier: 22.2.1 + version: 22.2.1(chokidar@5.0.0) '@angular-devkit/schematics': - specifier: 22.2.0 - version: 22.2.0(chokidar@5.0.0) + specifier: 22.2.1 + version: 22.2.1(chokidar@5.0.0) '@angular-eslint/eslint-plugin': specifier: 22.5.0 - version: 22.5.0(@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + version: 22.5.0(@typescript-eslint/utils@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2) '@angular-eslint/eslint-plugin-template': specifier: 22.5.0 - version: 22.5.0(@angular-eslint/template-parser@22.5.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(@typescript-eslint/types@8.70.1)(@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + version: 22.5.0(@angular-eslint/template-parser@22.5.0(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2))(@typescript-eslint/types@8.71.1)(@typescript-eslint/utils@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2) '@angular-eslint/template-parser': specifier: 22.5.0 - version: 22.5.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) + version: 22.5.0(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2) '@angular/build': - specifier: 22.2.0 - version: 22.2.0(@angular/compiler-cli@22.2.0(@angular/compiler@22.2.0)(typescript@6.0.3))(@angular/compiler@22.2.0)(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@types/node@26.6.3)(istanbul-lib-instrument@6.0.3(supports-color@10.2.2))(jiti@2.7.0)(less@4.6.4)(postcss@8.5.26)(rollup@4.60.4)(supports-color@10.2.2)(terser@5.46.2)(tslib@2.8.1)(tsx@4.23.12)(typescript@6.0.3)(vitest@4.1.11)(yaml@2.9.0) + specifier: 22.2.1 + version: 22.2.1(@angular/compiler-cli@22.2.1(@angular/compiler@22.2.1)(typescript@6.0.2))(@angular/compiler@22.2.1)(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)))(@types/node@26.6.4)(istanbul-lib-instrument@6.0.3(supports-color@10.2.2))(jiti@2.7.0)(less@4.6.4)(postcss@8.5.28)(rollup@4.60.4)(supports-color@10.2.2)(terser@5.46.2)(tslib@2.8.1)(tsx@4.23.15)(typescript@6.0.2)(vitest@5.0.3)(yaml@2.9.0) '@angular/cli': - specifier: 22.2.0 - version: 22.2.0(@types/node@26.6.3)(chokidar@5.0.0) + specifier: 22.2.1 + version: 22.2.1(@types/node@26.6.4)(chokidar@5.0.0) '@angular/compiler-cli': - specifier: 22.2.0 - version: 22.2.0(@angular/compiler@22.2.0)(typescript@6.0.3) + specifier: 22.2.1 + version: 22.2.1(@angular/compiler@22.2.1)(typescript@6.0.2) '@angular/language-service': - specifier: 22.2.0 - version: 22.2.0 + specifier: 22.2.1 + version: 22.2.1 '@eslint/js': specifier: 10.0.1 - version: 10.0.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)) + version: 10.0.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)) '@schematics/angular': - specifier: 22.2.0 - version: 22.2.0(chokidar@5.0.0) + specifier: 22.2.1 + version: 22.2.1(chokidar@5.0.0) '@types/feather-icons': specifier: ^4.29.4 version: 4.29.4 '@types/node': - specifier: 26.6.3 - version: 26.6.3 + specifier: 26.6.4 + version: 26.6.4 '@types/yargs': specifier: ^17.0.35 version: 17.0.35 '@typescript-eslint/eslint-plugin': - specifier: 8.70.1 - version: 8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + specifier: 8.71.1 + version: 8.71.1(@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) '@typescript-eslint/parser': - specifier: 8.70.1 - version: 8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + specifier: 8.71.1 + version: 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) '@typescript-eslint/utils': - specifier: ^8.68.0 - version: 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + specifier: ^8.71.1 + version: 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) '@vitest/coverage-v8': - specifier: 4.1.11 - version: 4.1.11(vitest@4.1.11) + specifier: 5.0.3 + version: 5.0.3(vitest@5.0.3) '@vitest/ui': - specifier: 4.1.11 - version: 4.1.11(vitest@4.1.11) + specifier: 5.0.3 + version: 5.0.3(vitest@5.0.3) concurrently: specifier: ^10.0.5 version: 10.0.5 dotenv: - specifier: ^17.4.2 - version: 17.4.2 + specifier: ^18.0.5 + version: 18.0.5 esbuild: specifier: ^0.28.2 version: 0.28.2 eslint: - specifier: 10.9.1 - version: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + specifier: 10.12.0 + version: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) eslint-config-prettier: specifier: 10.1.8 - version: 10.1.8(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)) + version: 10.1.8(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)) eslint-plugin-import: specifier: 2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + version: 2.32.0(@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) eslint-plugin-jsx-a11y: specifier: 6.10.2 - version: 6.10.2(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)) + version: 6.10.2(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)) eslint-plugin-react: specifier: 7.37.5 - version: 7.37.5(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)) + version: 7.37.5(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)) eslint-plugin-react-hooks: specifier: 7.1.1 - version: 7.1.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + version: 7.1.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) eslint-plugin-unused-imports: specifier: ^4.4.1 - version: 4.4.1(@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)) + version: 4.4.1(@typescript-eslint/eslint-plugin@8.71.1(@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)) fs-extra: - specifier: ^11.4.0 - version: 11.4.0 + specifier: ^11.4.1 + version: 11.4.1 globals: - specifier: ^17.11.0 - version: 17.11.0 + specifier: ^17.13.0 + version: 17.13.0 happy-dom: - specifier: ^20.11.6 - version: 20.11.6 + specifier: ^20.14.5 + version: 20.14.5 jiti: specifier: 2.7.0 version: 2.7.0 jsdom: - specifier: ~30.0.1 - version: 30.0.1 + specifier: ~30.1.2 + version: 30.1.2 jsonc-eslint-parser: specifier: ^3.3.0 version: 3.3.0 prettier: - specifier: ^3.9.6 - version: 3.9.6 + specifier: ^3.9.9 + version: 3.9.9 tsx: - specifier: ^4.23.12 - version: 4.23.12 + specifier: ^4.23.15 + version: 4.23.15 typedoc: specifier: ^0.28.20 - version: 0.28.20(typescript@6.0.3) + version: 0.28.20(typescript@6.0.2) typescript: - specifier: 6.0.3 - version: 6.0.3 + specifier: 6.0.2 + version: 6.0.2 typescript-eslint: - specifier: ^8.68.0 - version: 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + specifier: ^8.71.1 + version: 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) vite: - specifier: 8.2.2 - version: 8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) + specifier: 8.3.2 + version: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) vite-plugin-checker: specifier: ^0.14.5 - version: 0.14.5(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)) + version: 0.14.5(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)) vite-plugin-dts: - specifier: 5.0.3 - version: 5.0.3(@microsoft/api-extractor@7.56.0(@types/node@26.6.3))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(esbuild@0.28.2)(rolldown@1.2.2)(rollup@4.60.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0))(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)) + specifier: 5.1.2 + version: 5.1.2(@microsoft/api-extractor@7.56.0(@types/node@26.6.4))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(esbuild@0.28.2)(rolldown@1.2.2)(rollup@4.60.4)(supports-color@10.2.2)(typescript@6.0.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28)) vite-plugin-static-copy: specifier: ^4.1.1 - version: 4.1.1(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.1(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)) vitest: - specifier: 4.1.11 - version: 4.1.11(@types/node@26.6.3)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.6)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)) + specifier: 5.0.3 + version: 5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(happy-dom@20.14.5)(jsdom@30.1.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)) yargs: - specifier: ^18.1.0 - version: 18.1.0 + specifier: ^18.2.0 + version: 18.2.0 apps/colors-to-tokens-plugin: {} @@ -370,20 +370,20 @@ importers: version: 1.4.2 devDependencies: playwright: - specifier: ^1.62.1 - version: 1.62.1 + specifier: ^1.63.0 + version: 1.63.0 prettier: - specifier: ^3.9.6 - version: 3.9.6 + specifier: ^3.9.9 + version: 3.9.9 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 vite: - specifier: ^8.2.2 - version: 8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.97.3)(sass@1.101.0)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) + specifier: ^8.3.2 + version: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.97.3)(sass@1.101.0)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) vite-live-preview: specifier: ^0.4.0 - version: 0.4.0(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.97.3)(sass@1.101.0)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)) + version: 0.4.0(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.97.3)(sass@1.101.0)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)) apps/contrast-plugin: {} @@ -400,8 +400,8 @@ importers: apps/plugin-api-test-suite: devDependencies: playwright: - specifier: ^1.62.1 - version: 1.62.1 + specifier: ^1.63.0 + version: 1.63.0 apps/poc-state-plugin: {} @@ -433,13 +433,13 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@angular-devkit/architect@0.2202.0': - resolution: {integrity: sha512-HxY8JxLd3WxFj45BtDUHZ9I6hO4KebTz32UQ4ZkPh0EMK/TZdspbSgwd2lo1jE6a90knRAQkAsnBTKwWvGSm3A==} + '@angular-devkit/architect@0.2202.1': + resolution: {integrity: sha512-WGRAxFj+R9LC8HuK3DnDgxHo/XrgfklNQeNgjq/phTEVq5ZdWxc8kDoPA4uQz4vF+HY2eycAAtOPfMvj2TBEiQ==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular-devkit/core@22.2.0': - resolution: {integrity: sha512-dELBwXtJWUxFALdEVvdk8qK+XBa17EH5H9NZm+041/esjNi6SaZMusqBgr2PZKq7hD+O2jSCEHbey/hwkz2Awg==} + '@angular-devkit/core@22.2.1': + resolution: {integrity: sha512-2oiuWVbUg7pE9kh2grp+MF2ynys+tMWZ/mmoYlqWrhMS2n5oV304ir0/IvvCCSGz9qsbL2A1vZhnlI2UbTSNiA==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^5.0.0 @@ -447,8 +447,8 @@ packages: chokidar: optional: true - '@angular-devkit/schematics@22.2.0': - resolution: {integrity: sha512-yhp/LRzKHJOzo2gFm9+H5DKnsT9adi+ItO2pW0pW2B455sqZYE8RJaSku+TSlv+PvWSmbtFsPXNgSRLA6pSk8Q==} + '@angular-devkit/schematics@22.2.1': + resolution: {integrity: sha512-frHhFwu2TEfTZX5jfAYL2bztuG5mGggfJhiM9bo5/QtLAi49LtRbJxIktsu6vjJdujxeB2TKD7KqnXwNYgd41Q==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@angular-eslint/bundled-angular-compiler@22.5.0': @@ -483,8 +483,8 @@ packages: eslint: ^9.0.0 || ^10.0.0 typescript: '*' - '@angular/build@22.2.0': - resolution: {integrity: sha512-9ABYN9zp4DqDfZxiqyIMEB5iec6xIuVys+BfVjOFkwd3tCnOK+DnRtU8OmI7R8BxUZwLzbPqFffZzvCVVfHP7Q==} + '@angular/build@22.2.1': + resolution: {integrity: sha512-VA8EkK5Y6sJbBFyn2x5jzd3KByh29DDpI7n6hucT0sK6YkMJEvsojPUWet9ifwF3OL+ZrGvxYlAHyZGKVlqw0g==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: '@angular/compiler': ^22.0.0 @@ -494,7 +494,7 @@ packages: '@angular/platform-browser': ^22.0.0 '@angular/platform-server': ^22.0.0 '@angular/service-worker': ^22.0.0 - '@angular/ssr': ^22.2.0 + '@angular/ssr': ^22.2.1 istanbul-lib-instrument: ^6.0.0 karma: ^6.4.0 less: ^4.2.0 @@ -535,38 +535,38 @@ packages: vitest: optional: true - '@angular/cli@22.2.0': - resolution: {integrity: sha512-FW4RCNXaPpbBcS4w6fetTO6eRT0xySYGSdQ2DGREU30G1TBnpJdJ6TAvs8mTBPXNfFMQTyioR2ie7UkJljLJKg==} + '@angular/cli@22.2.1': + resolution: {integrity: sha512-tKMP68SZP+A9YCSRg0c+2e/+yWp7qiFrjCuQAomHK7owNaxHnLigAbra+8QA4xmWHoGaQk89NMmUPyA1BsBp8w==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular/common@22.2.0': - resolution: {integrity: sha512-BUKkncKwnaEcF/qatBOpzSIx2NthFZLYlTcE7iHwwsw+XQrwB9hU8Ca/l5HqaTJiMg0D8MjI/SWHnD4LFtNN+w==} + '@angular/common@22.2.1': + resolution: {integrity: sha512-OqBH3EY4i8e3PvnYDugZGipSs1HWs2ZrqoClndKtFDS9mQULSSuLoxjG9nF1J/c+yUqhuyZlHjkXWS0n32pEfw==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/core': 22.2.0 + '@angular/core': 22.2.1 rxjs: ^6.5.3 || ^7.4.0 - '@angular/compiler-cli@22.2.0': - resolution: {integrity: sha512-xDOvPlNv+RT7sE01wjuGFRMc5SDxWs3RZpGfO5NoI3a0jxM87UW49HmmgwQ6PaU0fya9mttNkHufhfOAbh0ZKQ==} + '@angular/compiler-cli@22.2.1': + resolution: {integrity: sha512-Nh8sCBl0JF6x+H7/1hemY76JeTRC66EfMULT4B3ryF9srRSLYc6X6RfPRtYvrIyjgr6h3jZgrNHlV76wiJaOcA==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} hasBin: true peerDependencies: - '@angular/compiler': 22.2.0 + '@angular/compiler': 22.2.1 typescript: '>=6.0 <6.1' peerDependenciesMeta: typescript: optional: true - '@angular/compiler@22.2.0': - resolution: {integrity: sha512-bnqql/HfhnhDXoS7isNaXJAsbKYszcH+nFVWA6YhiAVZhqo3f+XoQb30qeqNtEOKuuC7SZfaAJ3jBkAvyC5CKg==} + '@angular/compiler@22.2.1': + resolution: {integrity: sha512-KRSz0c/T5eEiFa+F1l9OppAebJj9pT8kU3ArBMlxR3v/vzhpCfuQsFVQ4xEFC7dEm3zZIdv9ASKAP6ocNC4D/Q==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} - '@angular/core@22.2.0': - resolution: {integrity: sha512-VhAgJ4GCdGgAy9+zADpSyEM9gt4pKg6+cqNeK3RBNeF3M3B8y7AWfD/nYRdM3Xzi152Pu+ipgFFNHpupCLTsvQ==} + '@angular/core@22.2.1': + resolution: {integrity: sha512-a8WlYTRiaMPz1KvLE5xjeWrLyZUuQX8bh6Ak7zajUci9iWsTUQefschjSwP1WKlin3JhM3XjmsTEDyZvSLIfRg==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/compiler': 22.2.0 + '@angular/compiler': 22.2.1 rxjs: ^6.5.3 || ^7.4.0 zone.js: ~0.15.0 || ~0.16.0 peerDependenciesMeta: @@ -575,55 +575,51 @@ packages: zone.js: optional: true - '@angular/forms@22.2.0': - resolution: {integrity: sha512-Nm8o1uBk31EuQGzsK5EunNBVFzTN9MuTYLSrmg1SCPTcava4qK3AzSpl1+Put+cZTTnGtEBIy45AFaxHR2oryg==} + '@angular/forms@22.2.1': + resolution: {integrity: sha512-xHM+PhrvYDJ+edPxh6PWhkE99tXWbsMcxdXCWF6QcyByX+bsW2rVlNZ7dumOsUle9MYa8Yj0lZFACUWbwFw+4w==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/common': 22.2.0 - '@angular/core': 22.2.0 - '@angular/platform-browser': 22.2.0 + '@angular/common': 22.2.1 + '@angular/core': 22.2.1 + '@angular/platform-browser': 22.2.1 rxjs: ^6.5.3 || ^7.4.0 - '@angular/language-service@22.2.0': - resolution: {integrity: sha512-J1uqzKP2/u5hWp8ShskUyuwp8J9EjbNnAjvmp4Klor9hwtlQ48e4wW8+Wd7ugrxbjDQ57dwHlOoeXuQLRCjonQ==} + '@angular/language-service@22.2.1': + resolution: {integrity: sha512-rLn5cEIZxcMu64USrAWpp/mQnq5UF443QsJNjvYJBi96BepLUtdneyKMsr9MtjBXkwOO7A0IlblX7xCVnRb1ag==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} - '@angular/platform-browser@22.2.0': - resolution: {integrity: sha512-XMUOyn8WARk9NdFGO3uRgdC3hPrp+2xsSYNeSkaEZ4jI3Pc23f22e4GI8Vayl/UOmShHVn/i+NWfTWh3SEbbSw==} + '@angular/platform-browser@22.2.1': + resolution: {integrity: sha512-KQ3oguemaSqyNP00yqEvPouY7NC4Lji+6S8yfjaWaUZjSth/GckpeP/Fo9HtMqd04y6MZ6lEb2hbQtTqFcSdEQ==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/animations': 22.2.0 - '@angular/common': 22.2.0 - '@angular/core': 22.2.0 + '@angular/animations': 22.2.1 + '@angular/common': 22.2.1 + '@angular/core': 22.2.1 peerDependenciesMeta: '@angular/animations': optional: true - '@angular/router@22.2.0': - resolution: {integrity: sha512-+pxHMwWWKg+ejS1vKa+tS91CDusCIK1MOSy6fVLc89McMOY/C8tsLrh1uJaMjklejsz+3XUhJgrLOvl3ISNz6w==} + '@angular/router@22.2.1': + resolution: {integrity: sha512-HJfJ4yA1znKd9YFtbDOJfKmD12x0PhS7LkmYdWlJRu2WkQZRBVrdGKdmbvV5UvrqCRllVWKpzw1TIZJM69rlcw==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/common': 22.2.0 - '@angular/core': 22.2.0 - '@angular/platform-browser': 22.2.0 + '@angular/common': 22.2.1 + '@angular/core': 22.2.1 + '@angular/platform-browser': 22.2.1 rxjs: ^6.5.3 || ^7.4.0 - '@asamuzakjp/css-color@6.0.5': - resolution: {integrity: sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==} - engines: {node: ^22.13.0 || >=24.0.0} + '@asamuzakjp/css-color@7.1.3': + resolution: {integrity: sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - '@asamuzakjp/dom-selector@8.3.0': - resolution: {integrity: sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==} - engines: {node: ^22.13.0 || >=24.0.0} + '@asamuzakjp/dom-selector@9.2.4': + resolution: {integrity: sha512-YnVzxDxLaqL0t7Q4wfcgHZjg55Wm6y5EaO7Bhu3q9J7wq/8iw3PhsnWiOQhYekx4VexnbkgWpTT6n+QIV7kczw==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/code-frame@8.0.0': - resolution: {integrity: sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/code-frame@8.0.6': resolution: {integrity: sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==} engines: {node: ^22.18.0 || >=24.11.0} @@ -632,10 +628,6 @@ packages: resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@8.0.0': - resolution: {integrity: sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/compat-data@8.0.5': resolution: {integrity: sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==} engines: {node: ^22.18.0 || >=24.11.0} @@ -656,10 +648,6 @@ packages: resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} - '@babel/generator@8.0.0': - resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/generator@8.0.6': resolution: {integrity: sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==} engines: {node: ^22.18.0 || >=24.11.0} @@ -668,10 +656,6 @@ packages: resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@8.0.0': - resolution: {integrity: sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-compilation-targets@8.0.6': resolution: {integrity: sha512-HTrtZ4+iA0+kzdbCgvdSk1U4pEl3BvpGnlUbualCGNbrVhhQvhgBzurTUmqpWyvyHbWYWaIfdx2mOlL9LJjsew==} engines: {node: ^22.18.0 || >=24.11.0} @@ -680,10 +664,6 @@ packages: resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - '@babel/helper-globals@8.0.0': - resolution: {integrity: sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-globals@8.0.6': resolution: {integrity: sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==} engines: {node: ^22.18.0 || >=24.11.0} @@ -702,10 +682,6 @@ packages: resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@8.0.0': - resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-string-parser@8.0.6': resolution: {integrity: sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==} engines: {node: ^22.18.0 || >=24.11.0} @@ -714,10 +690,6 @@ packages: resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@8.0.4': - resolution: {integrity: sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-validator-identifier@8.0.6': resolution: {integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==} engines: {node: ^22.18.0 || >=24.11.0} @@ -734,10 +706,6 @@ packages: resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/helpers@8.0.0': - resolution: {integrity: sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helpers@8.0.5': resolution: {integrity: sha512-fQtPOXjYOYv85PIdwotp2TJGVYOycX0PQq+l844fFAxOULtBy8BVF35GyeueX0r4KvDthqPH5xAI1clQPk/2uA==} engines: {node: ^22.18.0 || >=24.11.0} @@ -747,11 +715,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@8.0.4': - resolution: {integrity: sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==} - engines: {node: ^22.18.0 || >=24.11.0} - hasBin: true - '@babel/parser@8.0.6': resolution: {integrity: sha512-LpGDIYJAzc3Y3PT9x+FxBrGYu2PlWxLmyN7SwPTzaX0LSwEmFJBsRnPBqmaXF2r1v+Zhz6lNiEJ+7yKtM5+Ugg==} engines: {node: ^22.18.0 || >=24.11.0} @@ -769,10 +732,6 @@ packages: resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - '@babel/traverse@8.0.4': - resolution: {integrity: sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/traverse@8.0.6': resolution: {integrity: sha512-ilYV+qLStGzSKLipJlETdvdtpK3rAuwfCmCoGKv15MU4EoeIuLkespSNoNLFGOPSciEHx9TLWtN5mv0fz5w5mQ==} engines: {node: ^22.18.0 || >=24.11.0} @@ -785,10 +744,6 @@ packages: resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} - '@babel/types@8.0.4': - resolution: {integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/types@8.0.6': resolution: {integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==} engines: {node: ^22.18.0 || >=24.11.0} @@ -804,40 +759,46 @@ packages: '@bufbuild/protobuf@2.13.0': resolution: {integrity: sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==} - '@csstools/color-helpers@6.1.0': - resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + + '@csstools/color-helpers@6.1.2': + resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==} engines: {node: '>=20.19.0'} - '@csstools/css-calc@3.3.0': - resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + '@csstools/css-calc@3.4.3': + resolution: {integrity: sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==} engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-color-parser@4.1.10': - resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==} + '@csstools/css-color-parser@4.2.6': + resolution: {integrity: sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==} engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^4.0.0 - '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms': ^4.0.2 + '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-parser-algorithms@4.0.0': - resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + '@csstools/css-parser-algorithms@4.0.2': + resolution: {integrity: sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==} engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.2 - '@csstools/css-syntax-patches-for-csstree@1.1.7': - resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==} + '@csstools/css-syntax-patches-for-csstree@1.1.15': + resolution: {integrity: sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: css-tree: optional: true - '@csstools/css-tokenizer@4.0.0': - resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + '@csstools/css-tokenizer@4.0.2': + resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} engines: {node: '>=20.19.0'} '@emnapi/core@1.11.3': @@ -1049,8 +1010,8 @@ packages: resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.7.2': - resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@exodus/bytes@1.15.1': @@ -1062,6 +1023,15 @@ packages: '@noble/hashes': optional: true + '@exodus/bytes@1.16.0': + resolution: {integrity: sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@gerrit0/mini-shiki@3.23.0': resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} @@ -1213,15 +1183,6 @@ packages: '@types/node': optional: true - '@inquirer/type@4.0.7': - resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/type@4.1.1': resolution: {integrity: sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -1251,15 +1212,21 @@ packages: '@jridgewell/source-map@0.3.11': resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/sourcemap-codec@1.6.0': resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@listr2/prompt-adapter-inquirer@4.2.7': resolution: {integrity: sha512-k180vxSv2KqmnDxoato6pCqEJsov4U5C0EVmldZkAdNeVBxY48A/LM6TCNPGIzDK/iZSfsZP0Mw+gbWc5hXLyA==} engines: {node: '>=22.13.0'} @@ -1612,15 +1579,15 @@ packages: '@oxc-project/types@0.142.0': resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} - '@oxc-project/types@0.147.0': - resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} - '@oxc-project/types@0.149.0': resolution: {integrity: sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==} '@oxc-project/types@0.150.0': resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==} + '@oxc-project/types@0.152.0': + resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} + '@parcel/watcher-android-arm64@2.6.0': resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} engines: {node: '>= 10.0.0'} @@ -1725,8 +1692,8 @@ packages: yauzl: optional: true - '@rolldown/binding-android-arm-eabi@1.2.6': - resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} + '@rolldown/binding-android-arm-eabi@1.2.12': + resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] @@ -1737,14 +1704,14 @@ packages: cpu: [arm] os: [android] - '@rolldown/binding-android-arm64@1.2.2': - resolution: {integrity: sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==} + '@rolldown/binding-android-arm64@1.2.12': + resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.2.6': - resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==} + '@rolldown/binding-android-arm64@1.2.2': + resolution: {integrity: sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -1755,14 +1722,14 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.2': - resolution: {integrity: sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==} + '@rolldown/binding-darwin-arm64@1.2.12': + resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.2.6': - resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==} + '@rolldown/binding-darwin-arm64@1.2.2': + resolution: {integrity: sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -1773,14 +1740,14 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.2': - resolution: {integrity: sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==} + '@rolldown/binding-darwin-x64@1.2.12': + resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.6': - resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==} + '@rolldown/binding-darwin-x64@1.2.2': + resolution: {integrity: sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -1791,14 +1758,14 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.2': - resolution: {integrity: sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==} + '@rolldown/binding-freebsd-x64@1.2.12': + resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.2.6': - resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==} + '@rolldown/binding-freebsd-x64@1.2.2': + resolution: {integrity: sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -1809,14 +1776,14 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.2': - resolution: {integrity: sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': + resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.2.6': - resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.2': + resolution: {integrity: sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -1827,15 +1794,15 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.2': - resolution: {integrity: sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==} + '@rolldown/binding-linux-arm64-gnu@1.2.12': + resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.2.6': - resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==} + '@rolldown/binding-linux-arm64-gnu@1.2.2': + resolution: {integrity: sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1848,15 +1815,15 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.2': - resolution: {integrity: sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==} + '@rolldown/binding-linux-arm64-musl@1.2.12': + resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.2.6': - resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==} + '@rolldown/binding-linux-arm64-musl@1.2.2': + resolution: {integrity: sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1869,15 +1836,15 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.2': - resolution: {integrity: sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==} + '@rolldown/binding-linux-ppc64-gnu@1.2.12': + resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.2.6': - resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==} + '@rolldown/binding-linux-ppc64-gnu@1.2.2': + resolution: {integrity: sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] @@ -1890,15 +1857,15 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.2': - resolution: {integrity: sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==} + '@rolldown/binding-linux-s390x-gnu@1.2.12': + resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.6': - resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==} + '@rolldown/binding-linux-s390x-gnu@1.2.2': + resolution: {integrity: sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -1911,15 +1878,15 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.2': - resolution: {integrity: sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==} + '@rolldown/binding-linux-x64-gnu@1.2.12': + resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.6': - resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==} + '@rolldown/binding-linux-x64-gnu@1.2.2': + resolution: {integrity: sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1932,15 +1899,15 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.2': - resolution: {integrity: sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==} + '@rolldown/binding-linux-x64-musl@1.2.12': + resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.2.6': - resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==} + '@rolldown/binding-linux-x64-musl@1.2.2': + resolution: {integrity: sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1953,14 +1920,14 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.2': - resolution: {integrity: sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==} + '@rolldown/binding-openharmony-arm64@1.2.12': + resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.2.6': - resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==} + '@rolldown/binding-openharmony-arm64@1.2.2': + resolution: {integrity: sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -1971,14 +1938,14 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.2': - resolution: {integrity: sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==} + '@rolldown/binding-win32-arm64-msvc@1.2.12': + resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.2.6': - resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==} + '@rolldown/binding-win32-arm64-msvc@1.2.2': + resolution: {integrity: sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -1989,14 +1956,14 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.2': - resolution: {integrity: sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==} + '@rolldown/binding-win32-x64-msvc@1.2.12': + resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.6': - resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==} + '@rolldown/binding-win32-x64-msvc@1.2.2': + resolution: {integrity: sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2258,8 +2225,8 @@ packages: '@rushstack/ts-command-line@5.1.7': resolution: {integrity: sha512-Ugwl6flarZcL2nqH5IXFYk3UR3mBVDsVFlCQW/Oaqidvdb/5Ota6b/Z3JXWIdqV3rOR2/JrYoAHanWF5rgenXA==} - '@schematics/angular@22.2.0': - resolution: {integrity: sha512-IOD+T+S5LyVVuLAtLq/5k+WxnT9UpJecyDPvIT+AWug+mZJkd+HKVVHryenNJBaW29tIxNfoDuVFPEsZ4UKcNw==} + '@schematics/angular@22.2.1': + resolution: {integrity: sha512-L4Nl8D43znCzXwze9PtbQhG1GKlhWq9ip8Hz+e3LHgG3WpT5Tlr7aVsyHDqb9Qetuy0Mpd4tXE15STtGdA897w==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@seahax/deep-copy@0.1.0': @@ -2334,8 +2301,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@26.6.3': - resolution: {integrity: sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==} + '@types/node@26.6.4': + resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -2352,123 +2319,184 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.68.0': - resolution: {integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==} + '@typescript-eslint/eslint-plugin@8.71.1': + resolution: {integrity: sha512-nNlBf6HuotqMgZvSkWD5hNdemiBIxQ2NmrlgfcUIeZRs+BbakjscCQRq/Hb74EFVnFiSWWdL12JruqwBy3ERDw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.68.0 + '@typescript-eslint/parser': ^8.71.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/eslint-plugin@8.70.1': - resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.70.1 - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/parser@8.68.0': - resolution: {integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==} + '@typescript-eslint/parser@8.71.1': + resolution: {integrity: sha512-oM/5sAqz/l1v/zYK1qTOPrn2+JIrBGC1V6uNYucRO5gh6eT9vWJ/RhPbGE59byvZEUQ4VP6XkLBaS5Jti5jpvw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.70.1': - resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==} + '@typescript-eslint/project-service@8.71.1': + resolution: {integrity: sha512-Oijc9RUsohHjncg6iGi7gCtjnEjYt9FWv9u6ygK8uKcHK5eLjLAkqERQ9BaytbpceOFdk6kifPCS/zAZE+A99g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.71.1': + resolution: {integrity: sha512-1YUDZXdTnXLlob03SFHBipdyMBsDVm7zoSz0ytTJ39dk5J1TgsTK03VlR/dLXG/RLLUrPcYbD4rp23BmHxHP0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.71.1': + resolution: {integrity: sha512-oTvkml5SxXhgg+WWKhod5qWQICDXtOB+/Hi9VdPKFAANJ2b7YOgMffgvzXuqog7R6A/JtrzRf128dsxummyWhg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.71.1': + resolution: {integrity: sha512-kH3t3jZCYv2PZSBEWPKTariLrCaoF1CLoPitYFBJOhAGEwWnSRAtGEifrfrpBa4K9JlwWNZotgA5w3/wLSk/FQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.68.0': - resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==} + '@typescript-eslint/types@8.71.1': + resolution: {integrity: sha512-ybYnPTUwg3VQQOFEiBPLs8Jxl1ry2vXdn1kVKguomBbvxpetdu3MKTNDqFNUrlG8ylP9MpZosmXMFYlGQHk1oQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.71.1': + resolution: {integrity: sha512-pSKSEK1JJpHs6KiKVtABCE4iJK5d7FLFzDXQNBQ2oRY7LEd3UwOt3TZemTgu9syTIf56dJWlTn/pdmrO9oKUXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.70.1': - resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/scope-manager@8.68.0': - resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.70.1': - resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.68.0': - resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/tsconfig-utils@8.70.1': - resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/type-utils@8.68.0': - resolution: {integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==} + '@typescript-eslint/utils@8.71.1': + resolution: {integrity: sha512-CBjT6gfAz3DW2j9Q4moFnqt208Aq+506sVGXfpRcyAeqn0EpayL0mjdWoCO2t3RC7GewNrFxdeehuEaPg6DMzQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.70.1': - resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' - - '@typescript-eslint/types@8.68.0': - resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==} + '@typescript-eslint/visitor-keys@8.71.1': + resolution: {integrity: sha512-0GxiUGqMU0qXJt58cBsiG3DP8lUmBaFo2vChtUX+CT7PnR3v/VGLLrzVP4K9r1lNiZXjHYefMBbPunplr09v6w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.70.1': - resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] - '@typescript-eslint/typescript-estree@8.68.0': - resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] - '@typescript-eslint/typescript-estree@8.70.1': - resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.1.0' + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] - '@typescript-eslint/utils@8.68.0': - resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] - '@typescript-eslint/utils@8.70.1': - resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.1.0' + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] - '@typescript-eslint/visitor-keys@8.68.0': - resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] - '@typescript-eslint/visitor-keys@8.70.1': - resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] '@vitejs/plugin-basic-ssl@2.3.0': resolution: {integrity: sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==} @@ -2476,20 +2504,25 @@ packages: peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - '@vitest/coverage-v8@4.1.11': - resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==} + '@vitest/coverage-v8@5.0.3': + resolution: {integrity: sha512-+klsyz7BvT1vCU28Zkfzms1Ia78XD0V41U3FUtRaa3S+vOr/EXvK1O6BvVD7l1RzEjm6SONR2aybOvDDf9u0mQ==} peerDependencies: - '@vitest/browser': 4.1.11 - vitest: 4.1.11 + '@vitest/browser': 5.0.3 + vitest: 5.0.3 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.11': - resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + '@vitest/istanbul-lib-coverage@1.0.2': + resolution: {integrity: sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==} + engines: {node: '>=22'} - '@vitest/mocker@4.1.11': - resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + '@vitest/istanbul-lib-report@1.0.2': + resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==} + engines: {node: '>=22'} + + '@vitest/mocker@5.0.3': + resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2499,25 +2532,19 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.11': - resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + '@vitest/pretty-format@5.0.3': + resolution: {integrity: sha512-3hr5eV5z2ff0l2Q1vaKQbB4j3M3tElw8stDokEOTqXJB3Es5VrT8gBRIMY59gGuFp8Y61sWlWFK1aNUH5dHDYw==} - '@vitest/runner@4.1.11': - resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + '@vitest/spy@5.0.3': + resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==} - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - - '@vitest/ui@4.1.11': - resolution: {integrity: sha512-r/rwyKoev21mWdRGSEkZOqkQ2BYy68mwjihg9M90nNRbf4NGrgzZ4cj6JNCEwlOGJkbKeMgsjlykvwKUbRr7gw==} + '@vitest/ui@5.0.3': + resolution: {integrity: sha512-9dfvqDBvSbHM6y9YVXxAypkl2z7mrWozK/JFo237qI0cj/3M/kzDKiVZqzi3TE54Ler7Jditud7OMBZCyUvRHA==} peerDependencies: - vitest: 4.1.11 + vitest: 5.0.3 - '@vitest/utils@4.1.11': - resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + '@vitest/utils@5.0.3': + resolution: {integrity: sha512-77fDTZv8eu1CzlIfgPU2yrl5MXSUaUAOvgdZEeVm3X3WIxUubIX2SforqS6176lT32HCF6uBVMYVqfHxw7o6eg==} '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -2595,11 +2622,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.18.0: resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} @@ -2757,8 +2779,8 @@ packages: resolution: {integrity: sha512-8/lOOjT+PElYCnaUbmhgd3Uad5R1VTQ4Wg0k4rK8bX04m5WEqHNmnlLy3Q/lJ7dk25XA+72gayzc/skTfKKpcA==} engines: {node: '>=20.19.0'} - bidi-js@1.0.3: - resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} @@ -2791,6 +2813,9 @@ packages: resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} engines: {node: '>=4.0'} + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2924,9 +2949,9 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - data-urls@7.0.0: - resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-urls@8.0.0: + resolution: {integrity: sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==} + engines: {node: ^22.14.0 || >=24.0.0} data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} @@ -3006,9 +3031,10 @@ packages: resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==} engines: {node: '>=20.19.0'} - dotenv@17.4.2: - resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + dotenv@18.0.5: + resolution: {integrity: sha512-aBrGvt6KhjxbEnatqMWOMidftwsrCiCvqh1yNIGB9QyF0J+VcWkKDMsvYOEm7ZS82oiV+BT1sTOtIlAmUlTpwQ==} engines: {node: '>=12'} + hasBin: true dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} @@ -3067,8 +3093,8 @@ packages: resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==} engines: {node: '>= 0.4'} - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -3190,8 +3216,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.9.1: - resolution: {integrity: sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==} + eslint@10.12.0: + resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -3277,9 +3303,8 @@ packages: fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -3289,13 +3314,15 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} flatted@3.4.3: resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + flru@1.0.2: resolution: {integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==} engines: {node: '>=6'} @@ -3321,15 +3348,10 @@ packages: resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} engines: {node: '>=14.14'} - fs-extra@11.4.0: - resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} + fs-extra@11.4.1: + resolution: {integrity: sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==} engines: {node: '>=14.14'} - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3384,8 +3406,8 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - globals@17.11.0: - resolution: {integrity: sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==} + globals@17.13.0: + resolution: {integrity: sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==} engines: {node: '>=18'} globalthis@1.0.4: @@ -3399,8 +3421,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - happy-dom@20.11.6: - resolution: {integrity: sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg==} + happy-dom@20.14.5: + resolution: {integrity: sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ==} engines: {node: '>=20.0.0'} has-bigints@1.1.0: @@ -3426,6 +3448,10 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3440,16 +3466,19 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + hosted-git-info@10.1.1: resolution: {integrity: sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - html-encoding-sniffer@6.0.0: - resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-encoding-sniffer@7.0.0: + resolution: {integrity: sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==} + engines: {node: ^22.13.0 || >=24.0.0} htmlparser2@12.0.0: resolution: {integrity: sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==} @@ -3639,14 +3668,6 @@ packages: resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} engines: {node: '>=10'} - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -3668,8 +3689,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - jsdom@30.0.1: - resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} + jsdom@30.1.2: + resolution: {integrity: sha512-0FFE/jE1rppmVfUrJUgxqXjcwolZYIGtAgj1pTussMhNZxIxi7W/PvfWaMNroGi2B36X1IKHbexpZ9DhkoOPiQ==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} peerDependencies: canvas: ^3.2.3 @@ -3682,9 +3703,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -3717,8 +3735,8 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -3859,6 +3877,10 @@ packages: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3875,17 +3897,13 @@ packages: magic-string@1.4.1: resolution: {integrity: sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==} - magicast@0.5.3: - resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} - markdown-it@14.3.0: resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} hasBin: true @@ -4132,10 +4150,6 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - picomatch@4.0.7: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} @@ -4154,13 +4168,13 @@ packages: pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} - playwright-core@1.62.1: - resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} engines: {node: '>=20'} hasBin: true - playwright@1.62.1: - resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} engines: {node: '>=20'} hasBin: true @@ -4177,14 +4191,6 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} - engines: {node: ^10 || ^12 || >=14} - - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.28: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} @@ -4193,8 +4199,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.9.6: - resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + prettier@3.9.9: + resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} engines: {node: '>=14'} hasBin: true @@ -4241,6 +4247,10 @@ packages: engines: {node: '>=22.12.0'} hasBin: true + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -4292,13 +4302,13 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} - rolldown@1.2.2: - resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==} + rolldown@1.2.12: + resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.2.6: - resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} + rolldown@1.2.2: + resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -4652,9 +4662,6 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -4688,9 +4695,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} @@ -4753,10 +4757,6 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} @@ -4765,9 +4765,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - sync-child-process@1.0.2: resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} engines: {node: '>=16.0.0'} @@ -4831,11 +4828,12 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinybench@6.2.0: + resolution: {integrity: sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==} + engines: {node: '>=20.0.0'} - tinyexec@1.2.4: - resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} engines: {node: '>=18'} tinyglobby@0.2.17: @@ -4865,9 +4863,9 @@ packages: resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} engines: {node: '>=16'} - tr46@6.0.0: - resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} - engines: {node: '>=20'} + tr46@7.0.0: + resolution: {integrity: sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==} + engines: {node: ^22.14.0 || >=24.0.0} tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} @@ -4885,8 +4883,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.23.12: - resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + tsx@4.23.15: + resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} engines: {node: '>=18.0.0'} hasBin: true @@ -4920,8 +4918,8 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x - typescript-eslint@8.68.0: - resolution: {integrity: sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==} + typescript-eslint@8.71.1: + resolution: {integrity: sha512-oiNVPC3/NbV1FpaHU07l8O6ynqG9v4PAiW2WvTrAmZdR0f2HI/gE668ElMLCK7Xbsu5k2fK5C4fXNiq05j7/nQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -4932,8 +4930,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true @@ -4942,6 +4940,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -4955,8 +4958,8 @@ packages: undici-types@8.9.0: resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} - undici@8.9.0: - resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + undici@8.11.2: + resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} engines: {node: '>=22.19.0'} unicorn-magic@0.3.0: @@ -4967,8 +4970,8 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unplugin-dts@1.0.3: - resolution: {integrity: sha512-/GR887wfG4r1cWyt1UZsLRuMIjsmEbGkS9yJrz+0dsToHAYUD5CTyP3JMGVLv25j9K0mJcwAVvZno/aTuSUvNg==} + unplugin-dts@1.1.2: + resolution: {integrity: sha512-RDqy90XUmFj6rz4U5YoTMdYdaCJe2ZnQZaWIjkWS4952Gv0zBTQyqWtlGgVMPYHa982dURvyoc+sUOFIcDFv+w==} peerDependencies: '@microsoft/api-extractor': '>=7' '@rspack/core': ^1 @@ -5057,8 +5060,8 @@ packages: vue-tsc: optional: true - vite-plugin-dts@5.0.3: - resolution: {integrity: sha512-gIth6NdCEHWPiiRMCK3N6C8WjvdsrtEQrmsiG8h6Ov+lFP+b07Y+wcs9H0H7n146l0PDTYK4cQN1vgeG1pMdRQ==} + vite-plugin-dts@5.1.2: + resolution: {integrity: sha512-1bXUz836sWq6ewEMQre3ZtNFVfZk/92DppgCCKP8tWx/NWwV2wHuXlRHb2TztHjbikPxvyAGLGu7hVh94Plwww==} peerDependencies: '@microsoft/api-extractor': '>=7' rollup: '>=3' @@ -5077,49 +5080,6 @@ packages: peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - vite@8.2.2: - resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.4.0 || ^0.5.0 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vite@8.3.0: resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5163,23 +5123,66 @@ packages: yaml: optional: true - vitest@4.1.11: - resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vite@8.3.2: + resolution: {integrity: sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.0.3: + resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.11 - '@vitest/browser-preview': 4.1.11 - '@vitest/browser-webdriverio': 4.1.11 - '@vitest/coverage-istanbul': 4.1.11 - '@vitest/coverage-v8': 4.1.11 - '@vitest/ui': 4.1.11 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-preview': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.3 + '@vitest/coverage-v8': 5.0.3 + '@vitest/ui': 5.0.3 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -5207,9 +5210,9 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - w3c-xmlserializer@5.0.0: - resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} - engines: {node: '>=18'} + w3c-xmlserializer@6.0.0: + resolution: {integrity: sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} watchpack@2.5.2: resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} @@ -5250,12 +5253,8 @@ packages: resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} engines: {node: '>=20'} - whatwg-url@16.0.1: - resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - whatwg-url@17.1.0: - resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==} + whatwg-url@17.2.0: + resolution: {integrity: sha512-IhLIHNcBrzIixC5XXsbr4DxdjajyA5vQ5RwGZMtrUp+GbtE5Nrsge9QKau7jXrC0aTXLYGV0xiW7Tb4YS2aomw==} engines: {node: ^22.14.0 || >=24.0.0} which-boxed-primitive@1.1.1: @@ -5279,19 +5278,15 @@ packages: engines: {node: '>= 8'} hasBin: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} hasBin: true word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@10.0.0: - resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} - engines: {node: '>=20'} - wrap-ansi@10.0.2: resolution: {integrity: sha512-6OLRNZqRntVGBm26ghZ5eZhCuWWyOkRCSn8XsOIuq5stpCE/tjLbdozd93naILZig0lmD8ANfRwnhn0DLeXGlg==} engines: {node: '>=20'} @@ -5361,6 +5356,10 @@ packages: resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@18.2.0: + resolution: {integrity: sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -5391,14 +5390,14 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@angular-devkit/architect@0.2202.0(chokidar@5.0.0)': + '@angular-devkit/architect@0.2202.1(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 22.2.0(chokidar@5.0.0) + '@angular-devkit/core': 22.2.1(chokidar@5.0.0) rxjs: 7.8.2 transitivePeerDependencies: - chokidar - '@angular-devkit/core@22.2.0(chokidar@5.0.0)': + '@angular-devkit/core@22.2.1(chokidar@5.0.0)': dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) @@ -5409,9 +5408,9 @@ snapshots: optionalDependencies: chokidar: 5.0.0 - '@angular-devkit/schematics@22.2.0(chokidar@5.0.0)': + '@angular-devkit/schematics@22.2.1(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 22.2.0(chokidar@5.0.0) + '@angular-devkit/core': 22.2.1(chokidar@5.0.0) jsonc-parser: 3.3.1 magic-string: 1.4.1 ora: 9.4.1 @@ -5421,51 +5420,51 @@ snapshots: '@angular-eslint/bundled-angular-compiler@22.5.0': {} - '@angular-eslint/eslint-plugin-template@22.5.0(@angular-eslint/template-parser@22.5.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3))(@typescript-eslint/types@8.70.1)(@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': + '@angular-eslint/eslint-plugin-template@22.5.0(@angular-eslint/template-parser@22.5.0(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2))(@typescript-eslint/types@8.71.1)(@typescript-eslint/utils@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2)': dependencies: '@angular-eslint/bundled-angular-compiler': 22.5.0 - '@angular-eslint/template-parser': 22.5.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@angular-eslint/utils': 22.5.0(@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@angular-eslint/template-parser': 22.5.0(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2) + '@angular-eslint/utils': 22.5.0(@typescript-eslint/utils@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2) + '@typescript-eslint/types': 8.71.1 + '@typescript-eslint/utils': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) aria-query: 5.3.2 axobject-query: 4.1.0 - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - typescript: 6.0.3 + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) + typescript: 6.0.2 - '@angular-eslint/eslint-plugin@22.5.0(@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': + '@angular-eslint/eslint-plugin@22.5.0(@typescript-eslint/utils@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2)': dependencies: '@angular-eslint/bundled-angular-compiler': 22.5.0 - '@angular-eslint/utils': 22.5.0(@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3) - '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + '@angular-eslint/utils': 22.5.0(@typescript-eslint/utils@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2) + '@typescript-eslint/utils': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 - '@angular-eslint/template-parser@22.5.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': + '@angular-eslint/template-parser@22.5.0(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2)': dependencies: '@angular-eslint/bundled-angular-compiler': 22.5.0 - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) eslint-scope: 9.1.2 - typescript: 6.0.3 + typescript: 6.0.2 - '@angular-eslint/utils@22.5.0(@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.3)': + '@angular-eslint/utils@22.5.0(@typescript-eslint/utils@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(typescript@6.0.2)': dependencies: '@angular-eslint/bundled-angular-compiler': 22.5.0 - '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - typescript: 6.0.3 + '@typescript-eslint/utils': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) + typescript: 6.0.2 - '@angular/build@22.2.0(@angular/compiler-cli@22.2.0(@angular/compiler@22.2.0)(typescript@6.0.3))(@angular/compiler@22.2.0)(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)))(@types/node@26.6.3)(istanbul-lib-instrument@6.0.3(supports-color@10.2.2))(jiti@2.7.0)(less@4.6.4)(postcss@8.5.26)(rollup@4.60.4)(supports-color@10.2.2)(terser@5.46.2)(tslib@2.8.1)(tsx@4.23.12)(typescript@6.0.3)(vitest@4.1.11)(yaml@2.9.0)': + '@angular/build@22.2.1(@angular/compiler-cli@22.2.1(@angular/compiler@22.2.1)(typescript@6.0.2))(@angular/compiler@22.2.1)(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)))(@types/node@26.6.4)(istanbul-lib-instrument@6.0.3(supports-color@10.2.2))(jiti@2.7.0)(less@4.6.4)(postcss@8.5.28)(rollup@4.60.4)(supports-color@10.2.2)(terser@5.46.2)(tslib@2.8.1)(tsx@4.23.15)(typescript@6.0.2)(vitest@5.0.3)(yaml@2.9.0)': dependencies: '@ampproject/remapping': 2.3.0 - '@angular-devkit/architect': 0.2202.0(chokidar@5.0.0) - '@angular/compiler': 22.2.0 - '@angular/compiler-cli': 22.2.0(@angular/compiler@22.2.0)(typescript@6.0.3) + '@angular-devkit/architect': 0.2202.1(chokidar@5.0.0) + '@angular/compiler': 22.2.1 + '@angular/compiler-cli': 22.2.1(@angular/compiler@22.2.1)(typescript@6.0.2) '@babel/core': 8.0.5 - '@inquirer/confirm': 6.3.2(@types/node@26.6.3) + '@inquirer/confirm': 6.3.2(@types/node@26.6.4) '@parcel/watcher': 2.6.0 - '@vitejs/plugin-basic-ssl': 2.3.0(vite@8.3.0(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)) + '@vitejs/plugin-basic-ssl': 2.3.0(vite@8.3.0(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)) beasties: 0.5.4 browserslist: 4.28.7 chokidar: 5.0.0 @@ -5486,18 +5485,18 @@ snapshots: source-map-support: 0.5.21 tinyglobby: 0.2.17 tslib: 2.8.1 - typescript: 6.0.3 - vite: 8.3.0(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) + typescript: 6.0.2 + vite: 8.3.0(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) xxhash-wasm: 1.1.0 optionalDependencies: - '@angular/core': 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3) - '@angular/platform-browser': 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)) + '@angular/core': 22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/platform-browser': 22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)) istanbul-lib-instrument: 6.0.3(supports-color@10.2.2) less: 4.6.4 lmdb: 3.5.6 - postcss: 8.5.26 + postcss: 8.5.28 rollup: 4.60.4 - vitest: 4.1.11(@types/node@26.6.3)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.6)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(happy-dom@20.14.5)(jsdom@30.1.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)) transitivePeerDependencies: - '@types/node' - '@vitejs/devtools' @@ -5510,15 +5509,15 @@ snapshots: - tsx - yaml - '@angular/cli@22.2.0(@types/node@26.6.3)(chokidar@5.0.0)': + '@angular/cli@22.2.1(@types/node@26.6.4)(chokidar@5.0.0)': dependencies: - '@angular-devkit/architect': 0.2202.0(chokidar@5.0.0) - '@angular-devkit/core': 22.2.0(chokidar@5.0.0) - '@angular-devkit/schematics': 22.2.0(chokidar@5.0.0) - '@inquirer/prompts': 8.7.2(@types/node@26.6.3) - '@listr2/prompt-adapter-inquirer': 4.2.7(@inquirer/prompts@8.7.2(@types/node@26.6.3))(@types/node@26.6.3)(listr2@11.1.0) + '@angular-devkit/architect': 0.2202.1(chokidar@5.0.0) + '@angular-devkit/core': 22.2.1(chokidar@5.0.0) + '@angular-devkit/schematics': 22.2.1(chokidar@5.0.0) + '@inquirer/prompts': 8.7.2(@types/node@26.6.4) + '@listr2/prompt-adapter-inquirer': 4.2.7(@inquirer/prompts@8.7.2(@types/node@26.6.4))(@types/node@26.6.4)(listr2@11.1.0) '@modelcontextprotocol/server': 2.0.0 - '@schematics/angular': 22.2.0(chokidar@5.0.0) + '@schematics/angular': 22.2.1(chokidar@5.0.0) jsonc-parser: 3.3.1 listr2: 11.1.0 npm-package-arg: 14.0.0 @@ -5530,78 +5529,78 @@ snapshots: - '@types/node' - chokidar - '@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2)': + '@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2)': dependencies: - '@angular/core': 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/core': 22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/compiler-cli@22.2.0(@angular/compiler@22.2.0)(typescript@6.0.3)': + '@angular/compiler-cli@22.2.1(@angular/compiler@22.2.1)(typescript@6.0.2)': dependencies: - '@angular/compiler': 22.2.0 + '@angular/compiler': 22.2.1 '@babel/core': 8.0.1 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 chokidar: 5.0.0 convert-source-map: 1.9.0 reflect-metadata: 0.2.2 semver: 7.8.5 tslib: 2.8.1 - yargs: 18.1.0 + yargs: 18.2.0 optionalDependencies: - typescript: 6.0.3 + typescript: 6.0.2 - '@angular/compiler@22.2.0': + '@angular/compiler@22.2.1': dependencies: tslib: 2.8.1 - '@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)': + '@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)': dependencies: rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@angular/compiler': 22.2.0 + '@angular/compiler': 22.2.1 zone.js: 0.16.3 - '@angular/forms@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2)': + '@angular/forms@22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2)': dependencies: - '@angular/common': 22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) - '@angular/core': 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3) - '@angular/platform-browser': 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)) + '@angular/common': 22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + '@angular/core': 22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/platform-browser': 22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)) '@standard-schema/spec': 1.1.0 rxjs: 7.8.2 tslib: 2.8.1 zod: 4.6.5 - '@angular/language-service@22.2.0': {} + '@angular/language-service@22.2.1': {} - '@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))': + '@angular/platform-browser@22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))': dependencies: - '@angular/common': 22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) - '@angular/core': 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/common': 22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + '@angular/core': 22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3) tslib: 2.8.1 - '@angular/router@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2)': + '@angular/router@22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(@angular/platform-browser@22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)))(rxjs@7.8.2)': dependencies: - '@angular/common': 22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) - '@angular/core': 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3) - '@angular/platform-browser': 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)(zone.js@0.16.3)) + '@angular/common': 22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2) + '@angular/core': 22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3) + '@angular/platform-browser': 22.2.1(@angular/common@22.2.1(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3))(rxjs@7.8.2))(@angular/core@22.2.1(@angular/compiler@22.2.1)(rxjs@7.8.2)(zone.js@0.16.3)) rxjs: 7.8.2 tslib: 2.8.1 - '@asamuzakjp/css-color@6.0.5': + '@asamuzakjp/css-color@7.1.3': dependencies: - '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 - lru-cache: 11.5.2 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-color-parser': 4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + lru-cache: 11.5.3 - '@asamuzakjp/dom-selector@8.3.0': + '@asamuzakjp/dom-selector@9.2.4': dependencies: - bidi-js: 1.0.3 + bidi-js: 1.1.0 css-tree: 3.2.1 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.5.2 + lru-cache: 11.5.3 '@babel/code-frame@7.29.7': dependencies: @@ -5609,11 +5608,6 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/code-frame@8.0.0': - dependencies: - '@babel/helper-validator-identifier': 8.0.4 - js-tokens: 10.0.0 - '@babel/code-frame@8.0.6': dependencies: '@babel/helper-validator-identifier': 8.0.6 @@ -5621,8 +5615,6 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/compat-data@8.0.0': {} - '@babel/compat-data@8.0.5': {} '@babel/core@7.29.7(supports-color@10.2.2)': @@ -5647,14 +5639,14 @@ snapshots: '@babel/core@8.0.1': dependencies: - '@babel/code-frame': 8.0.0 - '@babel/generator': 8.0.0 - '@babel/helper-compilation-targets': 8.0.0 - '@babel/helpers': 8.0.0 - '@babel/parser': 8.0.4 + '@babel/code-frame': 8.0.6 + '@babel/generator': 8.0.6 + '@babel/helper-compilation-targets': 8.0.6 + '@babel/helpers': 8.0.5 + '@babel/parser': 8.0.6 '@babel/template': 8.0.0 - '@babel/traverse': 8.0.4 - '@babel/types': 8.0.4 + '@babel/traverse': 8.0.6 + '@babel/types': 8.0.6 '@types/gensync': 1.0.5 convert-source-map: 2.0.0 empathic: 2.0.1 @@ -5666,7 +5658,7 @@ snapshots: '@babel/core@8.0.5': dependencies: - '@babel/code-frame': 8.0.0 + '@babel/code-frame': 8.0.6 '@babel/generator': 8.0.6 '@babel/helper-compilation-targets': 8.0.6 '@babel/helpers': 8.0.5 @@ -5691,15 +5683,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/generator@8.0.0': - dependencies: - '@babel/parser': 8.0.4 - '@babel/types': 8.0.4 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@types/jsesc': 2.5.1 - jsesc: 3.1.0 - '@babel/generator@8.0.6': dependencies: '@babel/parser': 8.0.6 @@ -5717,14 +5700,6 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-compilation-targets@8.0.0': - dependencies: - '@babel/compat-data': 8.0.0 - '@babel/helper-validator-option': 8.0.0 - browserslist: 4.28.7 - lru-cache: 11.5.2 - semver: 7.8.5 - '@babel/helper-compilation-targets@8.0.6': dependencies: '@babel/compat-data': 8.0.5 @@ -5735,8 +5710,6 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-globals@8.0.0': {} - '@babel/helper-globals@8.0.6': {} '@babel/helper-module-imports@7.29.7(supports-color@10.2.2)': @@ -5757,14 +5730,10 @@ snapshots: '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-string-parser@8.0.0': {} - '@babel/helper-string-parser@8.0.6': {} '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-identifier@8.0.4': {} - '@babel/helper-validator-identifier@8.0.6': {} '@babel/helper-validator-option@7.29.7': {} @@ -5776,11 +5745,6 @@ snapshots: '@babel/template': 7.29.7 '@babel/types': 7.29.7 - '@babel/helpers@8.0.0': - dependencies: - '@babel/template': 8.0.0 - '@babel/types': 8.0.4 - '@babel/helpers@8.0.5': dependencies: '@babel/template': 8.0.0 @@ -5790,10 +5754,6 @@ snapshots: dependencies: '@babel/types': 7.29.7 - '@babel/parser@8.0.4': - dependencies: - '@babel/types': 8.0.4 - '@babel/parser@8.0.6': dependencies: '@babel/types': 8.0.6 @@ -5806,9 +5766,9 @@ snapshots: '@babel/template@8.0.0': dependencies: - '@babel/code-frame': 8.0.0 - '@babel/parser': 8.0.4 - '@babel/types': 8.0.4 + '@babel/code-frame': 8.0.6 + '@babel/parser': 8.0.6 + '@babel/types': 8.0.6 '@babel/traverse@7.29.7(supports-color@10.2.2)': dependencies: @@ -5822,16 +5782,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@8.0.4': - dependencies: - '@babel/code-frame': 8.0.0 - '@babel/generator': 8.0.0 - '@babel/helper-globals': 8.0.0 - '@babel/parser': 8.0.4 - '@babel/template': 8.0.0 - '@babel/types': 8.0.4 - obug: 2.1.4 - '@babel/traverse@8.0.6': dependencies: '@babel/code-frame': 8.0.6 @@ -5852,11 +5802,6 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/types@8.0.4': - dependencies: - '@babel/helper-string-parser': 8.0.0 - '@babel/helper-validator-identifier': 8.0.4 - '@babel/types@8.0.6': dependencies: '@babel/helper-string-parser': 8.0.6 @@ -5870,29 +5815,41 @@ snapshots: '@bufbuild/protobuf@2.13.0': {} - '@csstools/color-helpers@6.1.0': {} - - '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@cacheable/memory@2.2.0': dependencies: - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 - '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@cacheable/utils@2.5.0': dependencies: - '@csstools/color-helpers': 6.1.0 - '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-tokenizer': 4.0.0 + hashery: 1.5.1 + keyv: 5.6.0 - '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + '@csstools/color-helpers@6.1.2': {} + + '@csstools/css-calc@3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': dependencies: - '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 - '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)': + '@csstools/css-color-parser@4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/color-helpers': 6.1.2 + '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-tokenizer': 4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.15(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 - '@csstools/css-tokenizer@4.0.0': {} + '@csstools/css-tokenizer@4.0.2': {} '@emnapi/core@1.11.3': dependencies: @@ -5994,9 +5951,9 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))': dependencies: - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -6017,19 +5974,21 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))': + '@eslint/js@10.0.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))': optionalDependencies: - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.7.2': + '@eslint/plugin-kit@0.7.3': dependencies: '@eslint/core': 1.2.1 levn: 0.4.1 '@exodus/bytes@1.15.1': {} + '@exodus/bytes@1.16.0': {} + '@gerrit0/mini-shiki@3.23.0': dependencies: '@shikijs/engine-oniguruma': 3.23.0 @@ -6059,133 +6018,129 @@ snapshots: '@inquirer/ansi@2.0.8': {} - '@inquirer/checkbox@5.2.5(@types/node@26.6.3)': + '@inquirer/checkbox@5.2.5(@types/node@26.6.4)': dependencies: '@inquirer/ansi': 2.0.8 - '@inquirer/core': 12.0.3(@types/node@26.6.3) + '@inquirer/core': 12.0.3(@types/node@26.6.4) '@inquirer/figures': 2.0.9 - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/type': 4.1.1(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/confirm@6.3.2(@types/node@26.6.3)': + '@inquirer/confirm@6.3.2(@types/node@26.6.4)': dependencies: - '@inquirer/core': 12.0.3(@types/node@26.6.3) - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/core': 12.0.3(@types/node@26.6.4) + '@inquirer/type': 4.1.1(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/core@12.0.3(@types/node@26.6.3)': + '@inquirer/core@12.0.3(@types/node@26.6.4)': dependencies: '@inquirer/ansi': 2.0.8 '@inquirer/figures': 2.0.9 - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/type': 4.1.1(@types/node@26.6.4) cli-width: 4.1.0 fast-wrap-ansi: 0.2.2 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/editor@5.3.3(@types/node@26.6.3)': + '@inquirer/editor@5.3.3(@types/node@26.6.4)': dependencies: - '@inquirer/core': 12.0.3(@types/node@26.6.3) - '@inquirer/external-editor': 3.0.5(@types/node@26.6.3) - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/core': 12.0.3(@types/node@26.6.4) + '@inquirer/external-editor': 3.0.5(@types/node@26.6.4) + '@inquirer/type': 4.1.1(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/expand@5.1.5(@types/node@26.6.3)': + '@inquirer/expand@5.1.5(@types/node@26.6.4)': dependencies: - '@inquirer/core': 12.0.3(@types/node@26.6.3) - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/core': 12.0.3(@types/node@26.6.4) + '@inquirer/type': 4.1.1(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/external-editor@3.0.5(@types/node@26.6.3)': + '@inquirer/external-editor@3.0.5(@types/node@26.6.4)': dependencies: chardet: 2.2.0 iconv-lite: 0.7.3 optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 '@inquirer/figures@2.0.9': {} - '@inquirer/input@5.1.6(@types/node@26.6.3)': + '@inquirer/input@5.1.6(@types/node@26.6.4)': dependencies: - '@inquirer/core': 12.0.3(@types/node@26.6.3) - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/core': 12.0.3(@types/node@26.6.4) + '@inquirer/type': 4.1.1(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/number@4.2.3(@types/node@26.6.3)': + '@inquirer/number@4.2.3(@types/node@26.6.4)': dependencies: - '@inquirer/core': 12.0.3(@types/node@26.6.3) - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/core': 12.0.3(@types/node@26.6.4) + '@inquirer/type': 4.1.1(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/password@5.2.2(@types/node@26.6.3)': + '@inquirer/password@5.2.2(@types/node@26.6.4)': dependencies: '@inquirer/ansi': 2.0.8 - '@inquirer/core': 12.0.3(@types/node@26.6.3) - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/core': 12.0.3(@types/node@26.6.4) + '@inquirer/type': 4.1.1(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/prompts@8.7.2(@types/node@26.6.3)': + '@inquirer/prompts@8.7.2(@types/node@26.6.4)': dependencies: - '@inquirer/checkbox': 5.2.5(@types/node@26.6.3) - '@inquirer/confirm': 6.3.2(@types/node@26.6.3) - '@inquirer/editor': 5.3.3(@types/node@26.6.3) - '@inquirer/expand': 5.1.5(@types/node@26.6.3) - '@inquirer/input': 5.1.6(@types/node@26.6.3) - '@inquirer/number': 4.2.3(@types/node@26.6.3) - '@inquirer/password': 5.2.2(@types/node@26.6.3) - '@inquirer/rawlist': 5.3.5(@types/node@26.6.3) - '@inquirer/search': 4.3.3(@types/node@26.6.3) - '@inquirer/select': 5.2.5(@types/node@26.6.3) + '@inquirer/checkbox': 5.2.5(@types/node@26.6.4) + '@inquirer/confirm': 6.3.2(@types/node@26.6.4) + '@inquirer/editor': 5.3.3(@types/node@26.6.4) + '@inquirer/expand': 5.1.5(@types/node@26.6.4) + '@inquirer/input': 5.1.6(@types/node@26.6.4) + '@inquirer/number': 4.2.3(@types/node@26.6.4) + '@inquirer/password': 5.2.2(@types/node@26.6.4) + '@inquirer/rawlist': 5.3.5(@types/node@26.6.4) + '@inquirer/search': 4.3.3(@types/node@26.6.4) + '@inquirer/select': 5.2.5(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/rawlist@5.3.5(@types/node@26.6.3)': + '@inquirer/rawlist@5.3.5(@types/node@26.6.4)': dependencies: - '@inquirer/core': 12.0.3(@types/node@26.6.3) - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/core': 12.0.3(@types/node@26.6.4) + '@inquirer/type': 4.1.1(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/search@4.3.3(@types/node@26.6.3)': + '@inquirer/search@4.3.3(@types/node@26.6.4)': dependencies: - '@inquirer/core': 12.0.3(@types/node@26.6.3) + '@inquirer/core': 12.0.3(@types/node@26.6.4) '@inquirer/figures': 2.0.9 - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/type': 4.1.1(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/select@5.2.5(@types/node@26.6.3)': + '@inquirer/select@5.2.5(@types/node@26.6.4)': dependencies: '@inquirer/ansi': 2.0.8 - '@inquirer/core': 12.0.3(@types/node@26.6.3) + '@inquirer/core': 12.0.3(@types/node@26.6.4) '@inquirer/figures': 2.0.9 - '@inquirer/type': 4.1.1(@types/node@26.6.3) + '@inquirer/type': 4.1.1(@types/node@26.6.4) optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 - '@inquirer/type@4.0.7(@types/node@26.6.3)': + '@inquirer/type@4.1.1(@types/node@26.6.4)': optionalDependencies: - '@types/node': 26.6.3 - - '@inquirer/type@4.1.1(@types/node@26.6.3)': - optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 '@istanbuljs/schema@0.1.6': optional: true '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/gen-mapping@0.4.0-beta.0': @@ -6206,19 +6161,25 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 optional: true - '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 - '@listr2/prompt-adapter-inquirer@4.2.7(@inquirer/prompts@8.7.2(@types/node@26.6.3))(@types/node@26.6.3)(listr2@11.1.0)': + '@keyv/bigmap@1.3.1(keyv@5.6.0)': dependencies: - '@inquirer/prompts': 8.7.2(@types/node@26.6.3) - '@inquirer/type': 4.0.7(@types/node@26.6.3) + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@listr2/prompt-adapter-inquirer@4.2.7(@inquirer/prompts@8.7.2(@types/node@26.6.4))(@types/node@26.6.4)(listr2@11.1.0)': + dependencies: + '@inquirer/prompts': 8.7.2(@types/node@26.6.4) + '@inquirer/type': 4.1.1(@types/node@26.6.4) listr2: 11.1.0 transitivePeerDependencies: - '@types/node' @@ -6244,24 +6205,24 @@ snapshots: '@lmdb/lmdb-win32-x64@3.5.6': optional: true - '@microsoft/api-extractor-model@7.32.2(@types/node@26.6.3)': + '@microsoft/api-extractor-model@7.32.2(@types/node@26.6.4)': dependencies: '@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc-config': 0.18.1 - '@rushstack/node-core-library': 5.19.1(@types/node@26.6.3) + '@rushstack/node-core-library': 5.19.1(@types/node@26.6.4) transitivePeerDependencies: - '@types/node' optional: true - '@microsoft/api-extractor@7.56.0(@types/node@26.6.3)': + '@microsoft/api-extractor@7.56.0(@types/node@26.6.4)': dependencies: - '@microsoft/api-extractor-model': 7.32.2(@types/node@26.6.3) + '@microsoft/api-extractor-model': 7.32.2(@types/node@26.6.4) '@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc-config': 0.18.1 - '@rushstack/node-core-library': 5.19.1(@types/node@26.6.3) + '@rushstack/node-core-library': 5.19.1(@types/node@26.6.4) '@rushstack/rig-package': 0.6.0 - '@rushstack/terminal': 0.21.0(@types/node@26.6.3) - '@rushstack/ts-command-line': 5.1.7(@types/node@26.6.3) + '@rushstack/terminal': 0.21.0(@types/node@26.6.4) + '@rushstack/ts-command-line': 5.1.7(@types/node@26.6.4) diff: 8.0.4 lodash: 4.18.1 minimatch: 10.2.6 @@ -6481,12 +6442,12 @@ snapshots: '@oxc-project/types@0.142.0': optional: true - '@oxc-project/types@0.147.0': {} - '@oxc-project/types@0.149.0': {} '@oxc-project/types@0.150.0': {} + '@oxc-project/types@0.152.0': {} + '@parcel/watcher-android-arm64@2.6.0': optional: true @@ -6528,7 +6489,7 @@ snapshots: detect-libc: 2.1.2 is-glob: 4.0.3 node-addon-api: 7.1.1 - picomatch: 4.0.5 + picomatch: 4.0.7 optionalDependencies: '@parcel/watcher-android-arm64': 2.6.0 '@parcel/watcher-darwin-arm64': 2.6.0 @@ -6552,135 +6513,135 @@ snapshots: '@puppeteer/browsers@3.2.3': dependencies: modern-tar: 0.8.4 - yargs: 18.1.0 + yargs: 18.2.0 - '@rolldown/binding-android-arm-eabi@1.2.6': + '@rolldown/binding-android-arm-eabi@1.2.12': optional: true '@rolldown/binding-android-arm-eabi@1.2.8': optional: true - '@rolldown/binding-android-arm64@1.2.2': + '@rolldown/binding-android-arm64@1.2.12': optional: true - '@rolldown/binding-android-arm64@1.2.6': + '@rolldown/binding-android-arm64@1.2.2': optional: true '@rolldown/binding-android-arm64@1.2.8': optional: true - '@rolldown/binding-darwin-arm64@1.2.2': + '@rolldown/binding-darwin-arm64@1.2.12': optional: true - '@rolldown/binding-darwin-arm64@1.2.6': + '@rolldown/binding-darwin-arm64@1.2.2': optional: true '@rolldown/binding-darwin-arm64@1.2.8': optional: true - '@rolldown/binding-darwin-x64@1.2.2': + '@rolldown/binding-darwin-x64@1.2.12': optional: true - '@rolldown/binding-darwin-x64@1.2.6': + '@rolldown/binding-darwin-x64@1.2.2': optional: true '@rolldown/binding-darwin-x64@1.2.8': optional: true - '@rolldown/binding-freebsd-x64@1.2.2': + '@rolldown/binding-freebsd-x64@1.2.12': optional: true - '@rolldown/binding-freebsd-x64@1.2.6': + '@rolldown/binding-freebsd-x64@1.2.2': optional: true '@rolldown/binding-freebsd-x64@1.2.8': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.2': + '@rolldown/binding-linux-arm-gnueabihf@1.2.12': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + '@rolldown/binding-linux-arm-gnueabihf@1.2.2': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.2.8': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.2': + '@rolldown/binding-linux-arm64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.6': + '@rolldown/binding-linux-arm64-gnu@1.2.2': optional: true '@rolldown/binding-linux-arm64-gnu@1.2.8': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.2': + '@rolldown/binding-linux-arm64-musl@1.2.12': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.6': + '@rolldown/binding-linux-arm64-musl@1.2.2': optional: true '@rolldown/binding-linux-arm64-musl@1.2.8': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.2': + '@rolldown/binding-linux-ppc64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.6': + '@rolldown/binding-linux-ppc64-gnu@1.2.2': optional: true '@rolldown/binding-linux-ppc64-gnu@1.2.8': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.2': + '@rolldown/binding-linux-s390x-gnu@1.2.12': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.6': + '@rolldown/binding-linux-s390x-gnu@1.2.2': optional: true '@rolldown/binding-linux-s390x-gnu@1.2.8': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.2': + '@rolldown/binding-linux-x64-gnu@1.2.12': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.6': + '@rolldown/binding-linux-x64-gnu@1.2.2': optional: true '@rolldown/binding-linux-x64-gnu@1.2.8': optional: true - '@rolldown/binding-linux-x64-musl@1.2.2': + '@rolldown/binding-linux-x64-musl@1.2.12': optional: true - '@rolldown/binding-linux-x64-musl@1.2.6': + '@rolldown/binding-linux-x64-musl@1.2.2': optional: true '@rolldown/binding-linux-x64-musl@1.2.8': optional: true - '@rolldown/binding-openharmony-arm64@1.2.2': + '@rolldown/binding-openharmony-arm64@1.2.12': optional: true - '@rolldown/binding-openharmony-arm64@1.2.6': + '@rolldown/binding-openharmony-arm64@1.2.2': optional: true '@rolldown/binding-openharmony-arm64@1.2.8': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.2': + '@rolldown/binding-win32-arm64-msvc@1.2.12': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.6': + '@rolldown/binding-win32-arm64-msvc@1.2.2': optional: true '@rolldown/binding-win32-arm64-msvc@1.2.8': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.2': + '@rolldown/binding-win32-x64-msvc@1.2.12': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.6': + '@rolldown/binding-win32-x64-msvc@1.2.2': optional: true '@rolldown/binding-win32-x64-msvc@1.2.8': @@ -6692,7 +6653,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 - picomatch: 4.0.5 + picomatch: 4.0.7 optionalDependencies: rollup: 4.60.4 @@ -6831,7 +6792,7 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@rushstack/node-core-library@5.19.1(@types/node@26.6.3)': + '@rushstack/node-core-library@5.19.1(@types/node@26.6.4)': dependencies: ajv: 8.20.0 ajv-draft-04: 1.0.0(ajv@8.20.0) @@ -6842,12 +6803,12 @@ snapshots: resolve: 1.22.12 semver: 7.5.4 optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 optional: true - '@rushstack/problem-matcher@0.1.1(@types/node@26.6.3)': + '@rushstack/problem-matcher@0.1.1(@types/node@26.6.4)': optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 optional: true '@rushstack/rig-package@0.6.0': @@ -6856,18 +6817,18 @@ snapshots: strip-json-comments: 3.1.1 optional: true - '@rushstack/terminal@0.21.0(@types/node@26.6.3)': + '@rushstack/terminal@0.21.0(@types/node@26.6.4)': dependencies: - '@rushstack/node-core-library': 5.19.1(@types/node@26.6.3) - '@rushstack/problem-matcher': 0.1.1(@types/node@26.6.3) + '@rushstack/node-core-library': 5.19.1(@types/node@26.6.4) + '@rushstack/problem-matcher': 0.1.1(@types/node@26.6.4) supports-color: 8.1.1 optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 optional: true - '@rushstack/ts-command-line@5.1.7(@types/node@26.6.3)': + '@rushstack/ts-command-line@5.1.7(@types/node@26.6.4)': dependencies: - '@rushstack/terminal': 0.21.0(@types/node@26.6.3) + '@rushstack/terminal': 0.21.0(@types/node@26.6.4) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -6875,10 +6836,10 @@ snapshots: - '@types/node' optional: true - '@schematics/angular@22.2.0(chokidar@5.0.0)': + '@schematics/angular@22.2.1(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 22.2.0(chokidar@5.0.0) - '@angular-devkit/schematics': 22.2.0(chokidar@5.0.0) + '@angular-devkit/core': 22.2.1(chokidar@5.0.0) + '@angular-devkit/schematics': 22.2.1(chokidar@5.0.0) jsonc-parser: 3.3.1 oxc-parser: 0.150.0 typescript: 6.0.3 @@ -6964,7 +6925,7 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@26.6.3': + '@types/node@26.6.4': dependencies: undici-types: 8.9.0 @@ -6974,7 +6935,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 '@types/yargs-parser@21.0.3': {} @@ -6982,255 +6943,207 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.71.1(@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.68.0 - '@typescript-eslint/type-utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.68.0 - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + '@typescript-eslint/parser': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.71.1 + '@typescript-eslint/type-utils': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) + '@typescript-eslint/utils': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.71.1 + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) ignore: 7.0.6 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2)': dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.70.1 - '@typescript-eslint/type-utils': 8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.70.1 - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - ignore: 7.0.6 - natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.68.0 - '@typescript-eslint/types': 8.68.0 - '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.68.0 + '@typescript-eslint/scope-manager': 8.71.1 + '@typescript-eslint/types': 8.71.1 + '@typescript-eslint/typescript-estree': 8.71.1(supports-color@10.2.2)(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.71.1 debug: 4.4.3(supports-color@10.2.2) - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - typescript: 6.0.3 + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/project-service@8.71.1(supports-color@10.2.2)(typescript@6.0.2)': dependencies: - '@typescript-eslint/scope-manager': 8.70.1 - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.70.1 + '@typescript-eslint/tsconfig-utils': 8.71.1(typescript@6.0.2) + '@typescript-eslint/types': 8.71.1 debug: 4.4.3(supports-color@10.2.2) - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - typescript: 6.0.3 + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.68.0(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/scope-manager@8.71.1': dependencies: - '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3) - '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/types': 8.71.1 + '@typescript-eslint/visitor-keys': 8.71.1 + + '@typescript-eslint/tsconfig-utils@8.71.1(typescript@6.0.2)': + dependencies: + typescript: 6.0.2 + + '@typescript-eslint/type-utils@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2)': + dependencies: + '@typescript-eslint/types': 8.71.1 + '@typescript-eslint/typescript-estree': 8.71.1(supports-color@10.2.2)(typescript@6.0.2) + '@typescript-eslint/utils': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) debug: 4.4.3(supports-color@10.2.2) - typescript: 6.0.3 + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.70.1(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/types@8.71.1': {} + + '@typescript-eslint/typescript-estree@8.71.1(supports-color@10.2.2)(typescript@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3) - '@typescript-eslint/types': 8.70.1 - debug: 4.4.3(supports-color@10.2.2) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.68.0': - dependencies: - '@typescript-eslint/types': 8.68.0 - '@typescript-eslint/visitor-keys': 8.68.0 - - '@typescript-eslint/scope-manager@8.70.1': - dependencies: - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/visitor-keys': 8.70.1 - - '@typescript-eslint/tsconfig-utils@8.68.0(typescript@6.0.3)': - dependencies: - typescript: 6.0.3 - - '@typescript-eslint/tsconfig-utils@8.70.1(typescript@6.0.3)': - dependencies: - typescript: 6.0.3 - - '@typescript-eslint/type-utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': - dependencies: - '@typescript-eslint/types': 8.68.0 - '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - debug: 4.4.3(supports-color@10.2.2) - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': - dependencies: - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - debug: 4.4.3(supports-color@10.2.2) - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.68.0': {} - - '@typescript-eslint/types@8.70.1': {} - - '@typescript-eslint/typescript-estree@8.68.0(supports-color@10.2.2)(typescript@6.0.3)': - dependencies: - '@typescript-eslint/project-service': 8.68.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3) - '@typescript-eslint/types': 8.68.0 - '@typescript-eslint/visitor-keys': 8.68.0 + '@typescript-eslint/project-service': 8.71.1(supports-color@10.2.2)(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.71.1(typescript@6.0.2) + '@typescript-eslint/types': 8.71.1 + '@typescript-eslint/visitor-keys': 8.71.1 debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.70.1(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/utils@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2)': dependencies: - '@typescript-eslint/project-service': 8.70.1(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3) - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/visitor-keys': 8.70.1 - debug: 4.4.3(supports-color@10.2.2) - minimatch: 10.2.6 - semver: 7.8.5 - tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)) + '@typescript-eslint/scope-manager': 8.71.1 + '@typescript-eslint/types': 8.71.1 + '@typescript-eslint/typescript-estree': 8.71.1(supports-color@10.2.2)(typescript@6.0.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': + '@typescript-eslint/visitor-keys@8.71.1': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)) - '@typescript-eslint/scope-manager': 8.68.0 - '@typescript-eslint/types': 8.68.0 - '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)) - '@typescript-eslint/scope-manager': 8.70.1 - '@typescript-eslint/types': 8.70.1 - '@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - typescript: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.68.0': - dependencies: - '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/types': 8.71.1 eslint-visitor-keys: 5.0.1 - '@typescript-eslint/visitor-keys@8.70.1': - dependencies: - '@typescript-eslint/types': 8.70.1 - eslint-visitor-keys: 5.0.1 + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true - '@vitejs/plugin-basic-ssl@2.3.0(vite@8.3.0(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0))': - dependencies: - vite: 8.3.0(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true - '@vitest/coverage-v8@4.1.11(vitest@4.1.11)': + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@vitejs/plugin-basic-ssl@2.3.0(vite@8.3.0(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0))': + dependencies: + vite: 8.3.0(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) + + '@vitest/coverage-v8@5.0.3(vitest@5.0.3)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.11 + '@vitest/istanbul-lib-coverage': 1.0.2 + '@vitest/istanbul-lib-report': 1.0.2 ast-v8-to-istanbul: 1.0.5 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.3 + magicast: 0.5.5 obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.11(@types/node@26.6.3)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.6)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(happy-dom@20.14.5)(jsdom@30.1.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)) - '@vitest/expect@4.1.11': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - chai: 6.2.2 - tinyrainbow: 3.1.1 + '@vitest/istanbul-lib-coverage@1.0.2': {} - '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0))': + '@vitest/istanbul-lib-report@1.0.2': dependencies: - '@vitest/spy': 4.1.11 + '@vitest/istanbul-lib-coverage': 1.0.2 + + '@vitest/mocker@5.0.3(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.4.1 optionalDependencies: - vite: 8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) - '@vitest/pretty-format@4.1.11': + '@vitest/pretty-format@5.0.3': dependencies: tinyrainbow: 3.1.1 - '@vitest/runner@4.1.11': - dependencies: - '@vitest/utils': 4.1.11 - pathe: 2.0.3 + '@vitest/spy@5.0.3': {} - '@vitest/snapshot@4.1.11': + '@vitest/ui@5.0.3(vitest@5.0.3)': dependencies: - '@vitest/pretty-format': 4.1.11 - '@vitest/utils': 4.1.11 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.11': {} - - '@vitest/ui@4.1.11(vitest@4.1.11)': - dependencies: - '@vitest/utils': 4.1.11 + '@vitest/utils': 5.0.3 fflate: 0.8.3 - flatted: 3.4.3 + flatted: 3.4.4 pathe: 2.0.3 sirv: 3.0.2 - tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vitest: 4.1.11(@types/node@26.6.3)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.6)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(happy-dom@20.14.5)(jsdom@30.1.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)) - '@vitest/utils@4.1.11': + '@vitest/utils@5.0.3': dependencies: - '@vitest/pretty-format': 4.1.11 + '@vitest/pretty-format': 5.0.3 convert-source-map: 2.0.0 tinyrainbow: 3.1.1 @@ -7240,13 +7153,13 @@ snapshots: '@volar/source-map@2.4.28': {} - '@volar/typescript@2.4.28(typescript@6.0.3)': + '@volar/typescript@2.4.28(typescript@6.0.2)': dependencies: '@volar/language-core': 2.4.28 path-browserify: 1.0.1 vscode-uri: 3.1.0 optionalDependencies: - typescript: 6.0.3 + typescript: 6.0.2 '@webassemblyjs/ast@1.14.1': dependencies: @@ -7354,8 +7267,6 @@ snapshots: dependencies: acorn: 8.18.0 - acorn@8.17.0: {} - acorn@8.18.0: {} agent-base@6.0.2(supports-color@10.2.2): @@ -7543,11 +7454,11 @@ snapshots: domhandler: 6.0.1 htmlparser2: 12.0.0 picocolors: 1.1.1 - postcss: 8.5.26 + postcss: 8.5.28 postcss-media-query-parser: 0.2.3 - postcss-safe-parser: 7.0.1(postcss@8.5.26) + postcss-safe-parser: 7.0.1(postcss@8.5.28) - bidi-js@1.0.3: + bidi-js@1.1.0: dependencies: require-from-string: 2.0.2 @@ -7580,7 +7491,15 @@ snapshots: buffer-image-size@0.6.4: dependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 + + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 call-bind-apply-helpers@1.0.2: dependencies: @@ -7721,10 +7640,10 @@ snapshots: damerau-levenshtein@1.0.8: {} - data-urls@7.0.0: + data-urls@8.0.0: dependencies: whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 + whatwg-url: 17.2.0 transitivePeerDependencies: - '@noble/hashes' @@ -7805,7 +7724,7 @@ snapshots: domelementtype: 3.0.0 domhandler: 6.0.1 - dotenv@17.4.2: {} + dotenv@18.0.5: {} dunder-proto@1.0.1: dependencies: @@ -7920,7 +7839,7 @@ snapshots: iterator.prototype: 1.1.5 math-intrinsics: 1.1.0 - es-module-lexer@2.3.1: {} + es-module-lexer@2.3.2: {} es-object-atoms@1.1.1: dependencies: @@ -7982,9 +7901,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)): + eslint-config-prettier@10.1.8(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) eslint-import-resolver-node@0.3.10(supports-color@10.2.2): dependencies: @@ -7994,17 +7913,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.13.0(@typescript-eslint/parser@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-module-utils@2.13.0(@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: debug: 3.2.7(supports-color@10.2.2) optionalDependencies: - '@typescript-eslint/parser': 8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + '@typescript-eslint/parser': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) eslint-import-resolver-node: 0.3.10(supports-color@10.2.2) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -8013,9 +7932,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7(supports-color@10.2.2) doctrine: 2.1.0 - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) eslint-import-resolver-node: 0.3.10(supports-color@10.2.2) - eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) + eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint-import-resolver-node@0.3.10(supports-color@10.2.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -8027,13 +7946,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -8043,7 +7962,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -8052,18 +7971,18 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@7.1.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): + eslint-plugin-react-hooks@7.1.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2): dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/parser': 7.29.7 - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) hermes-parser: 0.25.1 zod: 4.6.5 zod-validation-error: 4.0.2(zod@4.6.5) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-react@7.37.5(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -8071,7 +7990,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.3.2 - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) estraverse: 5.3.0 hasown: 2.0.4 jsx-ast-utils: 3.3.5 @@ -8085,11 +8004,11 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.71.1(@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)): dependencies: - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.71.1(@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) eslint-scope@5.1.1: dependencies: @@ -8108,14 +8027,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2): + eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5(supports-color@10.2.2) '@eslint/config-helpers': 0.7.0 '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.2 + '@eslint/plugin-kit': 0.7.3 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -8130,7 +8049,7 @@ snapshots: esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 + file-entry-cache: 11.1.5 find-up: 5.0.0 glob-parent: 6.0.2 ignore: 5.3.2 @@ -8197,9 +8116,9 @@ snapshots: dependencies: fast-string-width: 3.0.2 - fdir@6.5.0(picomatch@4.0.5): + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 feather-icons@4.29.2: dependencies: @@ -8208,9 +8127,9 @@ snapshots: fflate@0.8.3: {} - file-entry-cache@8.0.0: + file-entry-cache@11.1.5: dependencies: - flat-cache: 4.0.1 + flat-cache: 6.1.23 fill-range@7.1.1: dependencies: @@ -8221,13 +8140,16 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@4.0.1: + flat-cache@6.1.23: dependencies: + cacheable: 2.5.0 flatted: 3.4.3 - keyv: 4.5.4 + hookified: 1.15.1 flatted@3.4.3: {} + flatted@3.4.4: {} + flru@1.0.2: {} follow-redirects@1.16.0(debug@4.4.3(supports-color@10.2.2)): @@ -8253,15 +8175,12 @@ snapshots: universalify: 2.0.1 optional: true - fs-extra@11.4.0: + fs-extra@11.4.1: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.1 universalify: 2.0.1 - fsevents@2.3.2: - optional: true - fsevents@2.3.3: optional: true @@ -8318,10 +8237,9 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-to-regexp@0.4.1: - optional: true + glob-to-regexp@0.4.1: {} - globals@17.11.0: {} + globals@17.13.0: {} globalthis@1.0.4: dependencies: @@ -8332,15 +8250,15 @@ snapshots: graceful-fs@4.2.11: {} - happy-dom@20.11.6: + happy-dom@20.14.5: dependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 buffer-image-size: 0.6.4 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.21.1 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -8363,6 +8281,10 @@ snapshots: dependencies: has-symbols: 1.1.0 + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -8377,18 +8299,20 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hookified@1.15.1: {} + + hookified@2.2.0: {} + hosted-git-info@10.1.1: dependencies: lru-cache: 11.5.2 - html-encoding-sniffer@6.0.0: + html-encoding-sniffer@7.0.0: dependencies: '@exodus/bytes': 1.15.1 transitivePeerDependencies: - '@noble/hashes' - html-escaper@2.0.2: {} - htmlparser2@12.0.0: dependencies: domelementtype: 3.0.0 @@ -8572,7 +8496,8 @@ snapshots: isexe@2.0.0: {} - istanbul-lib-coverage@3.2.2: {} + istanbul-lib-coverage@3.2.2: + optional: true istanbul-lib-instrument@6.0.3(supports-color@10.2.2): dependencies: @@ -8585,17 +8510,6 @@ snapshots: - supports-color optional: true - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -8607,7 +8521,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 merge-stream: 2.0.0 supports-color: 8.1.1 optional: true @@ -8621,36 +8535,33 @@ snapshots: js-tokens@4.0.0: {} - jsdom@30.0.1: + jsdom@30.1.2: dependencies: - '@asamuzakjp/css-color': 6.0.5 - '@asamuzakjp/dom-selector': 8.3.0 + '@asamuzakjp/css-color': 7.1.3 + '@asamuzakjp/dom-selector': 9.2.4 '@bramus/specificity': 2.4.2 - '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1) - '@exodus/bytes': 1.15.1 + '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1) + '@exodus/bytes': 1.16.0 css-tree: 3.2.1 - data-urls: 7.0.0 + data-urls: 8.0.0 decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0 + html-encoding-sniffer: 7.0.0 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.5.2 + lru-cache: 11.5.3 parse5: 8.0.1 saxes: 6.0.0 - symbol-tree: 3.2.4 tough-cookie: 6.0.2 - undici: 8.9.0 - w3c-xmlserializer: 5.0.0 + undici: 8.11.2 + w3c-xmlserializer: 6.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 17.1.0 + whatwg-url: 17.2.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' jsesc@3.1.0: {} - json-buffer@3.0.1: {} - json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -8684,9 +8595,9 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 - keyv@4.5.4: + keyv@5.6.0: dependencies: - json-buffer: 3.0.1 + '@keyv/serialize': 1.1.1 kolorist@1.8.0: {} @@ -8822,7 +8733,7 @@ snapshots: slice-ansi: 9.0.0 string-width: 8.2.2 strip-ansi: 7.2.0 - wrap-ansi: 10.0.0 + wrap-ansi: 10.0.2 loose-envify@1.4.0: dependencies: @@ -8830,6 +8741,8 @@ snapshots: lru-cache@11.5.2: {} + lru-cache@11.5.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -8843,13 +8756,13 @@ snapshots: magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 magic-string@1.4.1: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 - magicast@0.5.3: + magicast@0.5.5: dependencies: '@babel/parser': 7.29.7 '@babel/types': 7.29.8 @@ -8861,10 +8774,6 @@ snapshots: semver: 5.7.2 optional: true - make-dir@4.0.0: - dependencies: - semver: 7.8.5 - markdown-it@14.3.0: dependencies: argparse: 2.0.1 @@ -8915,7 +8824,7 @@ snapshots: mlly@1.8.2: dependencies: - acorn: 8.17.0 + acorn: 8.18.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.4 @@ -9141,8 +9050,6 @@ snapshots: picomatch@4.0.4: {} - picomatch@4.0.5: {} - picomatch@4.0.7: {} pify@4.0.1: @@ -9164,33 +9071,19 @@ snapshots: exsolve: 1.1.0 pathe: 2.0.3 - playwright-core@1.62.1: {} + playwright-core@1.63.0: {} - playwright@1.62.1: + playwright@1.63.0: dependencies: - playwright-core: 1.62.1 - optionalDependencies: - fsevents: 2.3.2 + playwright-core: 1.63.0 possible-typed-array-names@1.1.0: {} postcss-media-query-parser@0.2.3: {} - postcss-safe-parser@7.0.1(postcss@8.5.26): + postcss-safe-parser@7.0.1(postcss@8.5.28): dependencies: - postcss: 8.5.26 - - postcss@8.5.25: - dependencies: - nanoid: 3.3.18 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postcss@8.5.26: - dependencies: - nanoid: 3.3.18 - picocolors: 1.1.1 - source-map-js: 1.2.1 + postcss: 8.5.28 postcss@8.5.28: dependencies: @@ -9200,7 +9093,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier@3.9.6: {} + prettier@3.9.9: {} proc-log@7.0.0: {} @@ -9255,6 +9148,10 @@ snapshots: - utf-8-validate - yauzl + qified@0.10.1: + dependencies: + hookified: 2.2.0 + quansync@0.2.11: {} react-is@16.13.1: {} @@ -9316,6 +9213,27 @@ snapshots: retry@0.12.0: {} + rolldown@1.2.12: + dependencies: + '@oxc-project/types': 0.152.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.12 + '@rolldown/binding-android-arm64': 1.2.12 + '@rolldown/binding-darwin-arm64': 1.2.12 + '@rolldown/binding-darwin-x64': 1.2.12 + '@rolldown/binding-freebsd-x64': 1.2.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.12 + '@rolldown/binding-linux-arm64-gnu': 1.2.12 + '@rolldown/binding-linux-arm64-musl': 1.2.12 + '@rolldown/binding-linux-ppc64-gnu': 1.2.12 + '@rolldown/binding-linux-s390x-gnu': 1.2.12 + '@rolldown/binding-linux-x64-gnu': 1.2.12 + '@rolldown/binding-linux-x64-musl': 1.2.12 + '@rolldown/binding-openharmony-arm64': 1.2.12 + '@rolldown/binding-win32-arm64-msvc': 1.2.12 + '@rolldown/binding-win32-x64-msvc': 1.2.12 + rolldown@1.2.2: dependencies: '@oxc-project/types': 0.142.0 @@ -9337,27 +9255,6 @@ snapshots: '@rolldown/binding-win32-x64-msvc': 1.2.2 optional: true - rolldown@1.2.6: - dependencies: - '@oxc-project/types': 0.147.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.6 - '@rolldown/binding-android-arm64': 1.2.6 - '@rolldown/binding-darwin-arm64': 1.2.6 - '@rolldown/binding-darwin-x64': 1.2.6 - '@rolldown/binding-freebsd-x64': 1.2.6 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.6 - '@rolldown/binding-linux-arm64-gnu': 1.2.6 - '@rolldown/binding-linux-arm64-musl': 1.2.6 - '@rolldown/binding-linux-ppc64-gnu': 1.2.6 - '@rolldown/binding-linux-s390x-gnu': 1.2.6 - '@rolldown/binding-linux-x64-gnu': 1.2.6 - '@rolldown/binding-linux-x64-musl': 1.2.6 - '@rolldown/binding-openharmony-arm64': 1.2.6 - '@rolldown/binding-win32-arm64-msvc': 1.2.6 - '@rolldown/binding-win32-x64-msvc': 1.2.6 - rolldown@1.2.8: dependencies: '@oxc-project/types': 0.149.0 @@ -9728,8 +9625,6 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} - signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -9759,8 +9654,6 @@ snapshots: sprintf-js@1.0.3: optional: true - stackback@0.0.2: {} - std-env@4.2.0: {} stdin-discarder@0.3.2: {} @@ -9845,18 +9738,12 @@ snapshots: supports-color@10.2.2: {} - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - supports-color@8.1.1: dependencies: has-flag: 4.0.0 supports-preserve-symlinks-flag@1.0.0: {} - symbol-tree@3.2.4: {} - sync-child-process@1.0.2: dependencies: sync-message-port: 1.2.0 @@ -9866,17 +9753,17 @@ snapshots: tapable@2.3.3: optional: true - terser-webpack-plugin@5.6.1(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)): + terser-webpack-plugin@5.6.1(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28)(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.2 - webpack: 5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26) + webpack: 5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28) optionalDependencies: esbuild: 0.28.2 lightningcss: 1.33.0 - postcss: 8.5.26 + postcss: 8.5.28 optional: true terser@5.46.2: @@ -9889,14 +9776,14 @@ snapshots: tiny-invariant@1.3.3: {} - tinybench@2.9.0: {} + tinybench@6.2.0: {} - tinyexec@1.2.4: {} + tinyexec@1.3.1: {} tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 tinyrainbow@3.1.1: {} @@ -9916,15 +9803,15 @@ snapshots: dependencies: tldts: 7.4.9 - tr46@6.0.0: + tr46@7.0.0: dependencies: punycode: 2.3.1 tree-kill@1.2.2: {} - ts-api-utils@2.5.0(typescript@6.0.3): + ts-api-utils@2.5.0(typescript@6.0.2): dependencies: - typescript: 6.0.3 + typescript: 6.0.2 tsconfig-paths@3.15.0: dependencies: @@ -9935,7 +9822,7 @@ snapshots: tslib@2.8.1: {} - tsx@4.23.12: + tsx@4.23.15: dependencies: esbuild: 0.28.2 optionalDependencies: @@ -9980,33 +9867,56 @@ snapshots: typed-query-selector@2.12.2: {} - typedoc@0.28.20(typescript@6.0.3): + typedoc@0.28.20(typescript@6.0.2): dependencies: '@gerrit0/mini-shiki': 3.23.0 lunr: 2.3.9 markdown-it: 14.3.0 minimatch: 10.2.5 - typescript: 6.0.3 + typescript: 6.0.2 yaml: 2.9.0 - typescript-eslint@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3): + typescript-eslint@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.68.0(supports-color@10.2.2)(typescript@6.0.3) - '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) - typescript: 6.0.3 + '@typescript-eslint/eslint-plugin': 8.71.1(@typescript-eslint/parser@8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2))(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) + '@typescript-eslint/parser': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.71.1(supports-color@10.2.2)(typescript@6.0.2) + '@typescript-eslint/utils': 8.71.1(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) + typescript: 6.0.2 transitivePeerDependencies: - supports-color typescript@5.8.2: optional: true - typescript@5.9.3: {} + typescript@6.0.2: {} typescript@6.0.3: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + uc.micro@2.1.0: {} ufo@1.6.4: {} @@ -10020,39 +9930,40 @@ snapshots: undici-types@8.9.0: {} - undici@8.9.0: {} + undici@8.11.2: {} unicorn-magic@0.3.0: {} universalify@2.0.1: {} - unplugin-dts@1.0.3(@microsoft/api-extractor@7.56.0(@types/node@26.6.3))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(esbuild@0.28.2)(rolldown@1.2.2)(rollup@4.60.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0))(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)): + unplugin-dts@1.1.2(@microsoft/api-extractor@7.56.0(@types/node@26.6.4))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(esbuild@0.28.2)(rolldown@1.2.2)(rollup@4.60.4)(supports-color@10.2.2)(typescript@6.0.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: '@rollup/pluginutils': 5.4.0(rollup@4.60.4) - '@volar/typescript': 2.4.28(typescript@6.0.3) + '@volar/typescript': 2.4.28(typescript@6.0.2) compare-versions: 6.1.1 debug: 4.4.3(supports-color@10.2.2) + glob-to-regexp: 0.4.1 kolorist: 1.8.0 local-pkg: 1.2.1 magic-string: 0.30.21 - typescript: 6.0.3 + typescript: 6.0.2 unplugin: 2.3.11 optionalDependencies: - '@microsoft/api-extractor': 7.56.0(@types/node@26.6.3) + '@microsoft/api-extractor': 7.56.0(@types/node@26.6.4) '@rspack/core': 1.6.8(@swc/helpers@0.5.18) esbuild: 0.28.2 rolldown: 1.2.2 rollup: 4.60.4 - vite: 8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) - webpack: 5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26) + vite: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) + webpack: 5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28) transitivePeerDependencies: - supports-color unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 - acorn: 8.17.0 - picomatch: 4.0.5 + acorn: 8.18.0 + picomatch: 4.0.7 webpack-virtual-modules: 0.6.2 update-browserslist-db@1.2.3(browserslist@4.28.7): @@ -10071,20 +9982,20 @@ snapshots: verkit@0.3.2: {} - vite-live-preview@0.4.0(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.97.3)(sass@1.101.0)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)): + vite-live-preview@0.4.0(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.97.3)(sass@1.101.0)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: '@seahax/deep-copy': 0.1.0 '@seahax/semaphore': 0.5.1 '@types/ws': 8.18.1 escape-goat: 4.0.0 strip-ansi: 7.2.0 - vite: 8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.97.3)(sass@1.101.0)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.97.3)(sass@1.101.0)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) ws: 8.21.1 transitivePeerDependencies: - bufferutil - utf-8-validate - vite-plugin-checker@0.14.5(eslint@10.9.1(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)): + vite-plugin-checker@0.14.5(eslint@10.12.0(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: '@babel/code-frame': 7.29.7 chokidar: 5.0.0 @@ -10093,19 +10004,19 @@ snapshots: picomatch: 4.0.4 proper-lockfile: 4.1.2 tiny-invariant: 1.3.3 - vite: 8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) optionalDependencies: - eslint: 10.9.1(jiti@2.7.0)(supports-color@10.2.2) + eslint: 10.12.0(jiti@2.7.0)(supports-color@10.2.2) optionator: 0.9.4 - typescript: 6.0.3 + typescript: 6.0.2 - vite-plugin-dts@5.0.3(@microsoft/api-extractor@7.56.0(@types/node@26.6.3))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(esbuild@0.28.2)(rolldown@1.2.2)(rollup@4.60.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0))(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)): + vite-plugin-dts@5.1.2(@microsoft/api-extractor@7.56.0(@types/node@26.6.4))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(esbuild@0.28.2)(rolldown@1.2.2)(rollup@4.60.4)(supports-color@10.2.2)(typescript@6.0.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28)): dependencies: - unplugin-dts: 1.0.3(@microsoft/api-extractor@7.56.0(@types/node@26.6.3))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(esbuild@0.28.2)(rolldown@1.2.2)(rollup@4.60.4)(supports-color@10.2.2)(typescript@6.0.3)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0))(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)) + unplugin-dts: 1.1.2(@microsoft/api-extractor@7.56.0(@types/node@26.6.4))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(esbuild@0.28.2)(rolldown@1.2.2)(rollup@4.60.4)(supports-color@10.2.2)(typescript@6.0.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0))(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28)) optionalDependencies: - '@microsoft/api-extractor': 7.56.0(@types/node@26.6.3) + '@microsoft/api-extractor': 7.56.0(@types/node@26.6.4) rollup: 4.60.4 - vite: 8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) transitivePeerDependencies: - '@rspack/core' - '@vue/language-core' @@ -10115,23 +10026,23 @@ snapshots: - typescript - webpack - vite-plugin-static-copy@4.1.1(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)): + vite-plugin-static-copy@4.1.1(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: chokidar: 3.6.0 p-map: 7.0.4 picocolors: 1.1.1 tinyglobby: 0.2.17 - vite: 8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) - vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0): + vite@8.3.0(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.26 - rolldown: 1.2.6 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.8 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 @@ -10139,18 +10050,37 @@ snapshots: sass: 1.104.1 sass-embedded: 1.104.1 terser: 5.46.2 - tsx: 4.23.12 + tsx: 4.23.15 yaml: 2.9.0 - vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.97.3)(sass@1.101.0)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0): + vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.26 - rolldown: 1.2.6 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.12 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.6.3 + '@types/node': 26.6.4 + esbuild: 0.28.2 + fsevents: 2.3.3 + jiti: 2.7.0 + less: 4.6.4 + sass: 1.104.1 + sass-embedded: 1.104.1 + terser: 5.46.2 + tsx: 4.23.15 + yaml: 2.9.0 + + vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.97.3)(sass@1.101.0)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.12 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.6.4 esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 @@ -10158,62 +10088,37 @@ snapshots: sass: 1.101.0 sass-embedded: 1.97.3 terser: 5.46.2 - tsx: 4.23.12 + tsx: 4.23.15 yaml: 2.9.0 - vite@8.3.0(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0): + vitest@5.0.3(@types/node@26.6.4)(@vitest/coverage-v8@5.0.3)(@vitest/ui@5.0.3)(happy-dom@20.14.5)(jsdom@30.1.2)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)): dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.7 - postcss: 8.5.28 - rolldown: 1.2.6 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 26.6.3 - esbuild: 0.28.2 - fsevents: 2.3.3 - jiti: 2.7.0 - less: 4.6.4 - sass: 1.104.1 - sass-embedded: 1.104.1 - terser: 5.46.2 - tsx: 4.23.12 - yaml: 2.9.0 - - vitest@4.1.11(@types/node@26.6.3)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.6)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - es-module-lexer: 2.3.1 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0)) + chai: 6.2.2 + es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 + magic-string: 1.4.1 obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 + picomatch: 4.0.7 std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 + tinybench: 6.2.0 + tinyexec: 1.3.1 tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.2.2(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.12)(yaml@2.9.0) - why-is-node-running: 2.3.0 + vite: 8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.46.2)(tsx@4.23.15)(yaml@2.9.0) + why-is-node-running: 3.2.1 optionalDependencies: - '@types/node': 26.6.3 - '@vitest/coverage-v8': 4.1.11(vitest@4.1.11) - '@vitest/ui': 4.1.11(vitest@4.1.11) - happy-dom: 20.11.6 - jsdom: 30.0.1 + '@types/node': 26.6.4 + '@vitest/coverage-v8': 5.0.3(vitest@5.0.3) + '@vitest/ui': 5.0.3(vitest@5.0.3) + happy-dom: 20.14.5 + jsdom: 30.1.2 transitivePeerDependencies: - msw vscode-uri@3.1.0: {} - w3c-xmlserializer@5.0.0: + w3c-xmlserializer@6.0.0: dependencies: xml-name-validator: 5.0.0 @@ -10234,7 +10139,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26): + webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -10247,7 +10152,7 @@ snapshots: browserslist: 4.28.7 chrome-trace-event: 1.0.4 enhanced-resolve: 5.24.4 - es-module-lexer: 2.3.1 + es-module-lexer: 2.3.2 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -10257,7 +10162,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.1(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.26)) + terser-webpack-plugin: 5.6.1(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28)(webpack@5.106.2(esbuild@0.28.2)(lightningcss@1.33.0)(postcss@8.5.28)) watchpack: 2.5.2 webpack-sources: 3.5.1 transitivePeerDependencies: @@ -10279,18 +10184,10 @@ snapshots: whatwg-mimetype@5.0.0: {} - whatwg-url@16.0.1: + whatwg-url@17.2.0: dependencies: - '@exodus/bytes': 1.15.1 - tr46: 6.0.0 - webidl-conversions: 8.0.1 - transitivePeerDependencies: - - '@noble/hashes' - - whatwg-url@17.1.0: - dependencies: - '@exodus/bytes': 1.15.1 - tr46: 6.0.0 + '@exodus/bytes': 1.16.0 + tr46: 7.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: - '@noble/hashes' @@ -10340,19 +10237,10 @@ snapshots: dependencies: isexe: 2.0.0 - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 + why-is-node-running@3.2.1: {} word-wrap@1.2.5: {} - wrap-ansi@10.0.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 8.2.2 - strip-ansi: 7.2.0 - wrap-ansi@10.0.2: dependencies: ansi-styles: 6.2.3 @@ -10403,6 +10291,15 @@ snapshots: y18n: 5.0.8 yargs-parser: 22.0.0 + yargs@18.2.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.2.2 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yocto-queue@0.1.0: {} yoctocolors@2.2.0: {} From 3f6b7a79590256d6542e1add26826e4f21491f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Wed, 7 Oct 2026 10:42:54 +0200 Subject: [PATCH 042/145] :sparkles: Take changelog highlights from the release highlight label (#12154) * :sparkles: Take changelog highlights from the release highlight label The :rocket: section was picked by the agent on each run and kept forever, so a regeneration could add highlights product did not choose and nobody could drop one. Make the `release highlight` label the only source, as `breaking change` already is for :boom:, and have the report flag :rocket: entries that do not match the label. AI-assisted-by: claude-opus-5-5 * :books: Update changelog for 2.19.0 Regenerate the 2.19.0 section from the milestone with the update-changelog skill. Add the 79 closed issues that were missing, drop entries labelled `no changelog` and #9263, which is not in the milestone, and take :rocket: from the `release highlight` label. AI-assisted-by: claude-opus-5-5 --- .agents/skills/update-changelog/SKILL.md | 48 +++++++++---- CHANGES.md | 92 +++++++++++++++++++++--- scripts/changelog.py | 52 ++++++++++++-- 3 files changed, 164 insertions(+), 28 deletions(-) diff --git a/.agents/skills/update-changelog/SKILL.md b/.agents/skills/update-changelog/SKILL.md index bd279754bc..9d0276ef53 100644 --- a/.agents/skills/update-changelog/SKILL.md +++ b/.agents/skills/update-changelog/SKILL.md @@ -99,13 +99,18 @@ query is needed. **Breaking changes override everything:** an issue with the `breaking change` label goes under `### :boom: Breaking changes & Deprecations`, no matter its -issue type. This is the only label-based rule — it is an explicit exception -to the "never by labels" principle above. The `:boom:` subsection goes first -in the version, before `:rocket:` (matching the existing precedent). +issue type. This and `release highlight` below are the only label-based +rules — explicit exceptions to the "never by labels" principle above. The +`:boom:` subsection goes first in the version, before `:rocket:` (matching +the existing precedent). -**Preserve highlighted entries:** if an entry already sits under -`### :rocket: Epics and highlights`, keep it there when refreshing. Do not -move it down just because its type would place it under `:sparkles:`. +**Highlights come from the `release highlight` label:** an issue with that +label also gets an entry under `### :rocket: Epics and highlights`, on top of +its regular entry (usually `:sparkles:`). In the section being refreshed, +`:rocket:` lists exactly the labelled issues: never pick highlights yourself +and never keep an unlabelled entry there. Product decides the highlights by +labelling issues on GitHub, so a regeneration cannot undo that choice. Older +version sections are left as they are. **Community attribution:** if the issue or its fix PR has the `community contribution` label, add `(by @)` on the entry @@ -207,7 +212,10 @@ not trust cached data): 6. **Issue type changed to `Task`** → remove the entry. 7. **Breaking change misplaced** (issue has the `breaking change` label but sits in another section) → move the entry to `:boom:`. -8. **Missing valid issues** (closed, non-excluded, unreferenced anywhere) → +8. **Highlight out of sync** (`:rocket:` entry without the `release + highlight` label, or a labelled issue missing from `:rocket:`) → remove + the `:rocket:` entry or add it (step 7b). The regular entry stays. +9. **Missing valid issues** (closed, non-excluded, unreferenced anywhere) → add them to the current section per step 5. ### 7. Build the description text @@ -225,14 +233,22 @@ behavior: Insert the new version section right after the `# CHANGELOG` header with the `edit` tool and enough context for a unique match. -### 7b. Propose and populate `:rocket: Epics and highlights` +### 7b. Populate `:rocket: Epics and highlights` from the label -Create the subsection if missing (place it before `### :sparkles:`) and pick -2–5 of the most impactful/user-visible `:sparkles:` entries: new visible -features, big capabilities, items that make self-hosted users want to update. -`frontend/src/app/main/ui/releases/v2_.cljs` slide titles are optional -hints (they may not exist for every version). Every `:rocket:` entry MUST -carry issue AND PR references; never remove entries from a prior run. +List the milestone issues that pass step 2 exclusions and carry the +`release highlight` label: + +```bash +python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" \ + | python3 -c "import sys,json; [print(i['number'], i['title']) for i in json.load(sys.stdin) if 'release highlight' in i['labels']]" +``` + +Create the subsection if missing (place it before `### :sparkles:`) with one +entry per labelled issue, same text and references as its regular entry. +Every `:rocket:` entry MUST carry issue AND PR references. If no issue is +labelled, leave `:rocket:` out and tell the user: the highlights are +product's call, not the agent's. To change them, add or remove the label on +GitHub and re-run; never edit `:rocket:` by hand. ### 8. Cross-reference milestone PRs against the changelog @@ -264,6 +280,8 @@ pairing is misleading and a human must judge intent): 3. `:boom:` entry whose issue lacks the `breaking change` label. These stay listed in the report **and** in the changelog: either label the issue or move the entry to its regular section. +4. `:rocket:` out of sync with the `release highlight` label. Step 6 + pre-flight should have fixed it: if it shows up, re-run the workflow. **Highlight gaps are warnings, not anomalies:** missing `:rocket:` on a released X.Y.0 (patches never carry highlights); `:rocket:` entry without @@ -277,7 +295,7 @@ pre-flight. If one shows up in the report, re-run the workflow. - **Issue = changelog unit**, PR = implementation detail inline. - **Latest version first**, below the `# CHANGELOG` header. - **Issue Type decides the section — exclusively**, except `breaking change` - label → `:boom:` first. + label → `:boom:` first, and `release highlight` label → also `:rocket:`. - **User-facing descriptions** in imperative mood. - **Community attribution** uses the PR author, placed before the issue link. - **Only closed issues**; **Rejected** project status excludes. diff --git a/CHANGES.md b/CHANGES.md index 2e4f118bef..c464307e9d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,12 +6,12 @@ - Add configurable keyboard shortcuts [#9924](https://github.com/penpot/penpot/issues/9924) (PR: [#10237](https://github.com/penpot/penpot/pull/10237)) - Improve path operations and edition in the path editor [#10889](https://github.com/penpot/penpot/issues/10889) (PR: [#10807](https://github.com/penpot/penpot/pull/10807)) -- Add auto-linking of libraries during import based on slugified name [#9263](https://github.com/penpot/penpot/issues/9263) (PR: [#9958](https://github.com/penpot/penpot/pull/9958)) +- Add individual stroke widths per side (top, right, bottom, left) [#9854](https://github.com/penpot/penpot/issues/9854) (PR: [#10648](https://github.com/penpot/penpot/pull/10648)) +- Add support for internal libraries and file sync for Design Tokens [#9334](https://github.com/penpot/penpot/issues/9334) (PR: [#10293](https://github.com/penpot/penpot/pull/10293)) ### :bug: Bugs fixed - Fix copying text from Penpot to the clipboard not working on MS Windows [#11303](https://github.com/penpot/penpot/issues/11303) (PR: [#11305](https://github.com/penpot/penpot/pull/11305)) -- Fix performance issue with WebGL render [#11240](https://github.com/penpot/penpot/issues/11240) (PR: [#11259](https://github.com/penpot/penpot/pull/11259)) - Fix comment bubbles rendering on top of workspace dropdown menus [#10283](https://github.com/penpot/penpot/issues/10283) (PR: [#11201](https://github.com/penpot/penpot/pull/11201)) - Fix inconsistent Mixed label in blur options and numeric inputs across 24 locales (by @filipsajdak) [#11148](https://github.com/penpot/penpot/issues/11148) (PR: [#11151](https://github.com/penpot/penpot/pull/11151)) - Fix overlay shifting left when shown with top-center alignment in viewer prototype (by @filipsajdak) [#9048](https://github.com/penpot/penpot/issues/9048) (PR: [#10454](https://github.com/penpot/penpot/pull/10454)) @@ -21,31 +21,105 @@ - Fix plugin postMessage channel broadcasting messages to all plugins without origin validation [#10968](https://github.com/penpot/penpot/issues/10968) (PR: [#10970](https://github.com/penpot/penpot/pull/10970)) - Fix MCP plugin page navigation while connected crashing the workspace (by @makesomethingshit) [#11001](https://github.com/penpot/penpot/issues/11001) (PR: [#11521](https://github.com/penpot/penpot/pull/11521)) - Fix shortcut search never matching on key combination, only on action label [#11003](https://github.com/penpot/penpot/issues/11003) (PR: [#11081](https://github.com/penpot/penpot/pull/11081)) -- Fix Shift + special character key shortcut capturing the shifted character instead of the physical key [#11004](https://github.com/penpot/penpot/issues/11004) (PR: [#11081](https://github.com/penpot/penpot/pull/11081)) +- Fix Shift + special character key shortcut capturing the shifted character instead of the physical key [#11004](https://github.com/penpot/penpot/issues/11004) (PR: [#11081](https://github.com/penpot/penpot/pull/11081), [#11669](https://github.com/penpot/penpot/pull/11669)) - Fix reassigning the "Paste" shortcut not updating the UI or taking effect in the workspace [#11005](https://github.com/penpot/penpot/issues/11005) (PR: [#11081](https://github.com/penpot/penpot/pull/11081)) - Fix font-size dropdown clipping multi-digit values in Firefox (by @0xTHAC0) [#11008](https://github.com/penpot/penpot/issues/11008) (PR: [#11162](https://github.com/penpot/penpot/pull/11162), [#11500](https://github.com/penpot/penpot/pull/11500)) - Fix exporting shortcuts producing an invalid "toggle-fullscreen" entry that breaks re-import [#11032](https://github.com/penpot/penpot/issues/11032) (PR: [#11081](https://github.com/penpot/penpot/pull/11081)) - Fix plugin API missing permission checks in tokens, shapes, variants, flows, layouts, and user identity [#11137](https://github.com/penpot/penpot/issues/11137) (PR: [#11139](https://github.com/penpot/penpot/pull/11139)) - Fix library summary Redis cache keys omitting the tenant [#11407](https://github.com/penpot/penpot/issues/11407) (PR: [#11408](https://github.com/penpot/penpot/pull/11408)) - Fix active theme name in the inspect tab displaying an id instead of the name [#11437](https://github.com/penpot/penpot/issues/11437) (PR: [#11439](https://github.com/penpot/penpot/pull/11439)) -- Fix triple-click not selecting the full line in text editor v3 [#11483](https://github.com/penpot/penpot/issues/11483) (PR: [#11493](https://github.com/penpot/penpot/pull/11493)) -- Fix pasted text losing formatting on last lines after resizing and adding new lines from the top [#11501](https://github.com/penpot/penpot/issues/11501) (PR: [#11503](https://github.com/penpot/penpot/pull/11503)) - Fix variant property dropdown appearing empty and throwing an internal error when the component has no sibling variants [#11524](https://github.com/penpot/penpot/issues/11524) (PR: [#11499](https://github.com/penpot/penpot/pull/11499)) +- Fix invitation links not allowing registration when `disable-registration` is enabled [#5178](https://github.com/penpot/penpot/issues/5178) (PR: [#11912](https://github.com/penpot/penpot/pull/11912)) +- Fix Windows build failures caused by path resolution and start command conflicts (by @0xTHAC0) [#8637](https://github.com/penpot/penpot/issues/8637) (PR: [#11197](https://github.com/penpot/penpot/pull/11197)) +- Fix editing a path on an image, rectangle or ellipse corrupting the shape and failing backend validation [#10621](https://github.com/penpot/penpot/issues/10621) (PR: [#10807](https://github.com/penpot/penpot/pull/10807)) +- Fix board content disappearing when dragging a board near another board [#10689](https://github.com/penpot/penpot/issues/10689) (PR: [#11620](https://github.com/penpot/penpot/pull/11620)) +- Fix dashboard context menu being cut off for long lists, hiding the bottom options [#10724](https://github.com/penpot/penpot/issues/10724) (PR: [#11511](https://github.com/penpot/penpot/pull/11511)) +- Fix moving an element changing its size [#10751](https://github.com/penpot/penpot/issues/10751) (PR: [#11560](https://github.com/penpot/penpot/pull/11560)) +- Fix remote MCP websocket connecting but never receiving live browser commands [#10753](https://github.com/penpot/penpot/issues/10753) +- Fix stroke to path not working with path ends (by @ShreyashAgare26) [#10825](https://github.com/penpot/penpot/issues/10825) (PR: [#11614](https://github.com/penpot/penpot/pull/11614)) +- Fix border style not working with stroke per side [#10841](https://github.com/penpot/penpot/issues/10841) (PR: [#11883](https://github.com/penpot/penpot/pull/11883)) +- Fix multiple selection not showing stroke per side when all shapes share the same per side values [#10844](https://github.com/penpot/penpot/issues/10844) (PR: [#10913](https://github.com/penpot/penpot/pull/10913)) +- Fix stroke width tokens not overriding stroke per side values [#10850](https://github.com/penpot/penpot/issues/10850) (PR: [#10913](https://github.com/penpot/penpot/pull/10913)) +- Fix internal error when applying a dimension token to stroke width [#10918](https://github.com/penpot/penpot/issues/10918) (PR: [#10913](https://github.com/penpot/penpot/pull/10913)) +- Fix stroke per side button detaching the applied token [#10920](https://github.com/penpot/penpot/issues/10920) (PR: [#10913](https://github.com/penpot/penpot/pull/10913)) +- Fix WebSocket subscription handlers missing authorization checks [#11067](https://github.com/penpot/penpot/issues/11067) (PR: [#11069](https://github.com/penpot/penpot/pull/11069)) +- Fix project title being truncated in the dashboard despite available space [#11306](https://github.com/penpot/penpot/issues/11306) (PR: [#11564](https://github.com/penpot/penpot/pull/11564), [#11675](https://github.com/penpot/penpot/pull/11675)) +- Fix `rect` attribute NaN length error in the console when panning a file [#11327](https://github.com/penpot/penpot/issues/11327) (PR: [#11856](https://github.com/penpot/penpot/pull/11856)) +- Fix unclipped and rotated shapes being cut on render tiles [#11401](https://github.com/penpot/penpot/issues/11401) (PR: [#11423](https://github.com/penpot/penpot/pull/11423)) +- Fix Plugin API rejecting the documented `fontFamilies` applyToken property (by @makesomethingshit) [#11405](https://github.com/penpot/penpot/issues/11405) (PR: [#11566](https://github.com/penpot/penpot/pull/11566)) +- Fix flattening text ignoring non-zero letter spacing [#11462](https://github.com/penpot/penpot/issues/11462) (PR: [#11555](https://github.com/penpot/penpot/pull/11555)) +- Fix boolean results serialized as unordered subpaths, deforming them in the WebGL render and the exporter [#11482](https://github.com/penpot/penpot/issues/11482) (PR: [#11551](https://github.com/penpot/penpot/pull/11551)) +- Fix MCP reconnecting endlessly in inactive tabs and eventually freezing the browser (by @breken-ai) [#11510](https://github.com/penpot/penpot/issues/11510) (PR: [#11520](https://github.com/penpot/penpot/pull/11520), [#11906](https://github.com/penpot/penpot/pull/11906)) +- Fix dashed strokes losing their properties when changing color (by @Shlok1729) [#11549](https://github.com/penpot/penpot/issues/11549) (PR: [#11557](https://github.com/penpot/penpot/pull/11557)) +- Fix text decoration not applying per text span in the WebGL render [#11582](https://github.com/penpot/penpot/issues/11582) (PR: [#11583](https://github.com/penpot/penpot/pull/11583)) +- Fix binfile import exhausting backend memory when a ZIP JSON entry decompresses without limit [#11606](https://github.com/penpot/penpot/issues/11606) (PR: [#11607](https://github.com/penpot/penpot/pull/11607)) +- Fix modal not closing when going to the plans page via "See my current plan" (by @makesomethingshit) [#11611](https://github.com/penpot/penpot/issues/11611) (PR: [#11615](https://github.com/penpot/penpot/pull/11615)) +- Fix view-only share links disclosing unshared library pages when the file links a library [#11617](https://github.com/penpot/penpot/issues/11617) (PR: [#11627](https://github.com/penpot/penpot/pull/11627)) +- Fix canvas tiles showing only the background color while dragging the canvas background color picker [#11642](https://github.com/penpot/penpot/issues/11642) (PR: [#11643](https://github.com/penpot/penpot/pull/11643)) +- Fix title spacing and "added" pill padding in the libraries modal [#11645](https://github.com/penpot/penpot/issues/11645) (PR: [#11650](https://github.com/penpot/penpot/pull/11650)) +- Fix workspace crash when pasting before the page finishes loading [#11666](https://github.com/penpot/penpot/issues/11666) (PR: [#11674](https://github.com/penpot/penpot/pull/11674)) +- Fix tap area not matching the visible screen when zoomed out in the WebGL render (by @makesomethingshit) [#11689](https://github.com/penpot/penpot/issues/11689) (PR: [#11793](https://github.com/penpot/penpot/pull/11793)) +- Fix app crash when logging receives an empty level, and add the missing fatal level [#11690](https://github.com/penpot/penpot/issues/11690) (PR: [#11693](https://github.com/penpot/penpot/pull/11693)) +- Fix WebGL render ignoring drop and inner shadows on masked groups [#11697](https://github.com/penpot/penpot/issues/11697) (PR: [#11754](https://github.com/penpot/penpot/pull/11754)) +- Fix curves being created while Ctrl+dragging a path segment [#11702](https://github.com/penpot/penpot/issues/11702) (PR: [#11715](https://github.com/penpot/penpot/pull/11715)) +- Fix viewer role showing the search and replace option [#11706](https://github.com/penpot/penpot/issues/11706) (PR: [#11713](https://github.com/penpot/penpot/pull/11713)) +- Fix toggling the visibility of a child in a flex layout main component deleting the prototype interactions of its copies (by @ShreyashAgare26) [#11746](https://github.com/penpot/penpot/issues/11746) (PR: [#11788](https://github.com/penpot/penpot/pull/11788)) +- Fix cursor not visible on page load in the WebGL render [#11769](https://github.com/penpot/penpot/issues/11769) (PR: [#11770](https://github.com/penpot/penpot/pull/11770)) +- Fix library text styles detaching when copying and pasting an element [#11776](https://github.com/penpot/penpot/issues/11776) (PR: [#11785](https://github.com/penpot/penpot/pull/11785)) +- Fix Plugin API crashing the workspace when a shapechange listener is registered with an empty shape id [#11786](https://github.com/penpot/penpot/issues/11786) (PR: [#11787](https://github.com/penpot/penpot/pull/11787)) +- Fix borders not being fully rendered [#11805](https://github.com/penpot/penpot/issues/11805) (PR: [#11807](https://github.com/penpot/penpot/pull/11807)) +- Fix design token controls being enabled for fills and strokes below the first position [#11819](https://github.com/penpot/penpot/issues/11819) (PR: [#10913](https://github.com/penpot/penpot/pull/10913)) +- Fix viewer role showing the open libraries modal button [#11824](https://github.com/penpot/penpot/issues/11824) (PR: [#11849](https://github.com/penpot/penpot/pull/11849)) +- Fix extra padding on the "Sets" title in the tokens panel [#11826](https://github.com/penpot/penpot/issues/11826) (PR: [#11849](https://github.com/penpot/penpot/pull/11849)) +- Fix thumbnails failing on older Chromium browsers and each thumbnail rendering several times [#11878](https://github.com/penpot/penpot/issues/11878) (PR: [#11879](https://github.com/penpot/penpot/pull/11879)) +- Fix internal error when opening the libraries modal [#11943](https://github.com/penpot/penpot/issues/11943) (PR: [#11946](https://github.com/penpot/penpot/pull/11946)) +- Fix Plugin API `connectLibrary` linking unpublished files as libraries [#12001](https://github.com/penpot/penpot/issues/12001) (PR: [#12002](https://github.com/penpot/penpot/pull/12002)) ### :sparkles: New features & Enhancements - Make backend storage resilient to interrupted writes, missing files and stalled cleanup [#11344](https://github.com/penpot/penpot/issues/11344) (PR: [#11345](https://github.com/penpot/penpot/pull/11345)) -- Implement RTL support in the text editor v3 [#11262](https://github.com/penpot/penpot/issues/11262) - Improve path operations and edition in the path editor [#10889](https://github.com/penpot/penpot/issues/10889) (PR: [#10807](https://github.com/penpot/penpot/pull/10807)) - Add configurable keyboard shortcuts [#9924](https://github.com/penpot/penpot/issues/9924) (PR: [#10237](https://github.com/penpot/penpot/pull/10237)) -- Add auto-linking of libraries during import based on slugified name [#9263](https://github.com/penpot/penpot/issues/9263) (PR: [#9958](https://github.com/penpot/penpot/pull/9958)) -- Add support for internal libraries and file sync for Design Tokens [#9334](https://github.com/penpot/penpot/issues/9334) +- Add support for internal libraries and file sync for Design Tokens [#9334](https://github.com/penpot/penpot/issues/9334) (PR: [#10293](https://github.com/penpot/penpot/pull/10293)) - Warn self-hosted users when their Penpot version is outdated and surface what they're missing [#10497](https://github.com/penpot/penpot/issues/10497) (PR: [#11411](https://github.com/penpot/penpot/pull/11411)) - Add dedicated RPC methods for plugin registry operations with permission validation [#10952](https://github.com/penpot/penpot/issues/10952) (PR: [#10957](https://github.com/penpot/penpot/pull/10957)) - Document MCP and internal resolver environment variables (by @ShreyashAgare26) [#11318](https://github.com/penpot/penpot/issues/11318) (PR: [#11572](https://github.com/penpot/penpot/pull/11572)) - Add tokens source indicator to assets tab [#11365](https://github.com/penpot/penpot/issues/11365) (PR: [#11439](https://github.com/penpot/penpot/pull/11439)) -- Export multiple fills to SVG [#11466](https://github.com/penpot/penpot/issues/11466) (PR: [#11467](https://github.com/penpot/penpot/pull/11467)) - Add Penpot-specific board size presets (file thumbnail, template cover, plugin icon/cover) [#11561](https://github.com/penpot/penpot/issues/11561) (PR: [#11565](https://github.com/penpot/penpot/pull/11565)) +- Apply tokens to stroke per side [#9842](https://github.com/penpot/penpot/issues/9842) (PR: [#10913](https://github.com/penpot/penpot/pull/10913)) +- Add individual stroke widths per side (top, right, bottom, left) [#9854](https://github.com/penpot/penpot/issues/9854) (PR: [#10648](https://github.com/penpot/penpot/pull/10648)) +- Expand the token tree right after importing it (by @AKnassa) [#9819](https://github.com/penpot/penpot/issues/9819) (PR: [#10664](https://github.com/penpot/penpot/pull/10664)) +- Update the selection size badge live during resize, replacing the resize tooltip [#10259](https://github.com/penpot/penpot/issues/10259) +- Add color variants, rotation correction, viewport flip and hide-when-too-small to the selection size badge [#10264](https://github.com/penpot/penpot/issues/10264) (PR: [#11884](https://github.com/penpot/penpot/pull/11884)) +- Support single-file SVG export with the WebGL render [#10546](https://github.com/penpot/penpot/issues/10546) +- Persist the hide resolved comments preference and separate the filter options in the comments menu [#10686](https://github.com/penpot/penpot/issues/10686) (PR: [#10694](https://github.com/penpot/penpot/pull/10694)) +- Add rich link previews (Open Graph) for shared Penpot links [#10699](https://github.com/penpot/penpot/issues/10699) (PR: [#10700](https://github.com/penpot/penpot/pull/10700)) +- Add 3x export scale option [#10740](https://github.com/penpot/penpot/issues/10740) (PR: [#11598](https://github.com/penpot/penpot/pull/11598)) +- Show a validation message in the input instead of a toast when the email is already in use (by @ShreyashAgare26) [#10890](https://github.com/penpot/penpot/issues/10890) (PR: [#11647](https://github.com/penpot/penpot/pull/11647)) +- Take `waitForLayoutUpdate` into account in MCP instructions and implementation [#10901](https://github.com/penpot/penpot/issues/10901) (PR: [#11433](https://github.com/penpot/penpot/pull/11433)) +- Add a standard context menu (Cut, Copy, Paste, Select all) on right-click when editing text [#10914](https://github.com/penpot/penpot/issues/10914) (PR: [#11677](https://github.com/penpot/penpot/pull/11677)) +- Process export requests as export jobs [#11133](https://github.com/penpot/penpot/issues/11133) (PR: [#11296](https://github.com/penpot/penpot/pull/11296)) +- Enable closed schemas for RPC method parameters [#11135](https://github.com/penpot/penpot/issues/11135) (PR: [#11136](https://github.com/penpot/penpot/pull/11136)) +- Add Content-Security-Policy and Strict-Transport-Security headers to the frontend image [#11374](https://github.com/penpot/penpot/issues/11374) (PR: [#11375](https://github.com/penpot/penpot/pull/11375)) +- Send a notification email when the password is changed or set [#11392](https://github.com/penpot/penpot/issues/11392) (PR: [#11393](https://github.com/penpot/penpot/pull/11393)) +- Lock accounts after repeated failed login attempts [#11397](https://github.com/penpot/penpot/issues/11397) (PR: [#11402](https://github.com/penpot/penpot/pull/11402), [#11895](https://github.com/penpot/penpot/pull/11895)) +- Enforce an absolute maximum session duration [#11444](https://github.com/penpot/penpot/issues/11444) (PR: [#11447](https://github.com/penpot/penpot/pull/11447)) +- Add a loading message to the libraries section [#11452](https://github.com/penpot/penpot/issues/11452) (PR: [#11594](https://github.com/penpot/penpot/pull/11594)) +- Resize image bounds independently from the image content on the canvas [#11516](https://github.com/penpot/penpot/issues/11516) (PR: [#11590](https://github.com/penpot/penpot/pull/11590)) +- Limit binfile import database connection time and stop rescanning ZIP entries [#11579](https://github.com/penpot/penpot/issues/11579) (PR: [#11584](https://github.com/penpot/penpot/pull/11584)) +- Apply the same validation rules in the Plugin API as in the UI [#11586](https://github.com/penpot/penpot/issues/11586) (PR: [#11587](https://github.com/penpot/penpot/pull/11587)) +- Fetch only the caller's share link in the view-only bundle (by @makesomethingshit) [#11633](https://github.com/penpot/penpot/issues/11633) (PR: [#11657](https://github.com/penpot/penpot/pull/11657)) +- Add Prometheus metrics for backend storage operations [#11676](https://github.com/penpot/penpot/issues/11676) (PR: [#11700](https://github.com/penpot/penpot/pull/11700)) +- Make "Fix when scrolling" work in the WebGL render [#11684](https://github.com/penpot/penpot/issues/11684) (PR: [#11752](https://github.com/penpot/penpot/pull/11752)) +- Complete Catalan translations [#11739](https://github.com/penpot/penpot/issues/11739) (PR: [#11741](https://github.com/penpot/penpot/pull/11741)) +- Remove the optional client-provided id from creation RPC endpoints [#11783](https://github.com/penpot/penpot/issues/11783) (PR: [#11784](https://github.com/penpot/penpot/pull/11784)) +- Allow color tokens for the canvas background [#11791](https://github.com/penpot/penpot/issues/11791) (PR: [#11923](https://github.com/penpot/penpot/pull/11923)) +- Show a "no assets yet" message for empty files in the libraries modal [#11811](https://github.com/penpot/penpot/issues/11811) (PR: [#11818](https://github.com/penpot/penpot/pull/11818)) +- Upgrade the MCP SDK to v2 and adopt the new protocol version [#11827](https://github.com/penpot/penpot/issues/11827) (PR: [#11841](https://github.com/penpot/penpot/pull/11841)) +- Remove legacy SSE support from the MCP server [#11846](https://github.com/penpot/penpot/issues/11846) (PR: [#11841](https://github.com/penpot/penpot/pull/11841)) +- Delete selected path handles with Backspace or Delete [#11858](https://github.com/penpot/penpot/issues/11858) (PR: [#11896](https://github.com/penpot/penpot/pull/11896)) +- Use the tooltip component in the token pill info [#11886](https://github.com/penpot/penpot/issues/11886) (PR: [#11900](https://github.com/penpot/penpot/pull/11900)) +- Hide the duplicate cursor when alt-dragging a shape that cannot be duplicated (by @0xTHAC0) [#11905](https://github.com/penpot/penpot/issues/11905) (PR: [#11195](https://github.com/penpot/penpot/pull/11195)) ## 2.18.3 diff --git a/scripts/changelog.py b/scripts/changelog.py index b9106f201f..fbba459fbd 100644 --- a/scripts/changelog.py +++ b/scripts/changelog.py @@ -265,6 +265,7 @@ def cmd_report(args: argparse.Namespace) -> None: for pr in pr_by_num.values(): unknown_issues.update(pr.get("closing_issues", [])) unknown_issues |= collect_changelog_issues(extract_subsection(section, r"### :boom:")) + unknown_issues |= collect_changelog_issues(extract_subsection(section, r"### :rocket:")) unknown_issues -= set(issue_milestone_cache) external_issues: dict[int, dict] = {} if unknown_issues: @@ -375,6 +376,36 @@ def cmd_report(args: argparse.Namespace) -> None: } ) + # Type F: :rocket: out of sync with the `release highlight` label. The + # label is the only source of highlights: every labelled milestone + # issue goes under :rocket:, and nothing else does. + anomalies_f = [] + rocket_issues = collect_changelog_issues(extract_subsection(section, r"### :rocket:")) + for issue_num in sorted(rocket_issues): + labels = get_issue_labels(issue_num) + if labels is None: + continue + if "release highlight" not in labels: + issue = issue_by_num.get(issue_num) or external_issues.get(issue_num) or {} + anomalies_f.append( + { + "issue": issue_num, + "issue_title": issue.get("title", ""), + "reason": "under :rocket: without the `release highlight` label", + } + ) + for issue_num, issue in sorted(issue_by_num.items()): + if issue_excluded(issue) or issue_num in rocket_issues: + continue + if "release highlight" in issue.get("labels", []): + anomalies_f.append( + { + "issue": issue_num, + "issue_title": issue.get("title", ""), + "reason": "labelled `release highlight` but missing from :rocket:", + } + ) + # Type C: released X.Y.0 sections without a :rocket: subsection. # Patches (X.Y.Z with Z != 0) never carry :rocket: — only minors/majors. anomalies_c = [] @@ -428,12 +459,13 @@ def cmd_report(args: argparse.Namespace) -> None: ) f.write("---\n\n") - n_a, n_b, n_c, n_d, n_e = ( + n_a, n_b, n_c, n_d, n_e, n_f = ( len(anomalies_a), len(anomalies_b), len(anomalies_c), len(anomalies_d), len(anomalies_e), + len(anomalies_f), ) f.write("## Summary\n\n") @@ -444,13 +476,14 @@ def cmd_report(args: argparse.Namespace) -> None: f"- **PR in {milestone}, closing issue in a different milestone:** {n_b}\n" ) f.write(f"- **:boom: entry without breaking change label:** {n_e}\n") - f.write(f"- **Total anomalies:** {n_a + n_b + n_e}\n") + f.write(f"- **:rocket: out of sync with release highlight label:** {n_f}\n") + f.write(f"- **Total anomalies:** {n_a + n_b + n_e + n_f}\n") f.write( f"- **Released X.Y.0 version missing :rocket: section (gap):** {n_c}\n" ) f.write(f"- **:rocket: entry without issue AND PR references (gap):** {n_d}\n\n") - if n_a or n_b or n_e: + if n_a or n_b or n_e or n_f: f.write("## Anomalies\n\n") f.write( "Types A and B are milestone mismatches between an issue in the changelog " @@ -461,6 +494,9 @@ def cmd_report(args: argparse.Namespace) -> None: "removing the misleading entry from the changelog.\n\n" "Type E entries stay in the changelog: either label the issue " "as `breaking change` or move the entry out of `:boom:`.\n\n" + "Type F is a rule violation: `:rocket:` must list exactly the " + "issues labelled `release highlight`. Fix the label on GitHub " + "or re-run the workflow.\n\n" ) if n_a: @@ -512,6 +548,14 @@ def cmd_report(args: argparse.Namespace) -> None: for e in anomalies_e: f.write(f"- ⚠️ {issue_link_title(e['issue'], e['issue_title'][:80])}\n") f.write("\n") + + if n_f: + f.write("\n### :rocket: out of sync with release highlight label\n\n") + for e in anomalies_f: + f.write( + f"- ⚠️ {issue_link_title(e['issue'], e['issue_title'][:80])}: {e['reason']}\n" + ) + f.write("\n") else: f.write( "✅ No anomalies found. All (issue, PR) pairs in the changelog have aligned milestone assignments.\n\n" @@ -546,7 +590,7 @@ def cmd_report(args: argparse.Namespace) -> None: for d in anomalies_d: f.write(f"- **{d['version']}**: `{d['line']}`\n") f.write("\n") - elif not (n_a or n_b or n_e): + elif not (n_a or n_b or n_e or n_f): f.write( "✅ No highlight gaps found. All released X.Y.0 versions have properly referenced :rocket: entries.\n\n" ) From 2a6e92a7d9a5520365276f7b9a6ae59d478159ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 7 Oct 2026 10:45:17 +0200 Subject: [PATCH 043/145] :bug: Preserve path segments when joining backend URIs (#12152) --- backend/src/app/config.clj | 10 +++++++++- backend/test/backend_tests/config_test.clj | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 10a97b7bee..6c04c82861 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -432,6 +432,13 @@ (or (c/get config :file-clean-delay) (ct/duration {:days 2}))) +(defn- join-path-segments + [segments] + (let [path (->> segments (map str) (str/join "/"))] + (->> (str/split path #"/") + (remove str/blank?) + (str/join "/")))) + (defn join-uri "Join path segments onto a base URI, preserving a potential subpath (same semantics as the frontend config). The base is normalized with @@ -440,7 +447,8 @@ [base & segments] (assert (not (some #(str/starts-with? % "/") segments)) "URI segments must be relative (no leading slash)") - (str (apply u/join (u/ensure-path-slash base) segments))) + (str (u/join (u/ensure-path-slash base) + (join-path-segments segments)))) (defn get-public-uri "Canonical public URI builder: `join-uri` over the configured diff --git a/backend/test/backend_tests/config_test.clj b/backend/test/backend_tests/config_test.clj index 863be00a5e..3fb8a96e57 100644 --- a/backend/test/backend_tests/config_test.clj +++ b/backend/test/backend_tests/config_test.clj @@ -34,6 +34,15 @@ (t/is (= "https://nitrate.example.com/api/teams/123" (cf/join-uri "https://nitrate.example.com/" "api/teams/123"))))) +(t/deftest join-uri-joins-multiple-segments + (t/testing "path segments are appended instead of replacing previous segments" + (t/is (= "https://nitrate.example.com/admin-console/api/teams/team-id/users/profile-id" + (cf/join-uri "https://nitrate.example.com/admin-console" + "api/teams/" + "team-id" + "users/" + "profile-id"))))) + (t/deftest join-uri-rejects-leading-slash (t/testing "a leading slash would drop the subpath, so it fails fast" (t/is (thrown? AssertionError From a9bf53adbfc72dc8b3dfc630085e084ec0981ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 7 Oct 2026 11:10:02 +0200 Subject: [PATCH 044/145] Revert ":bug: Preserve path segments when joining backend URIs (#12152)" (#12156) This reverts commit 2a6e92a7d9a5520365276f7b9a6ae59d478159ee. --- backend/src/app/config.clj | 10 +--------- backend/test/backend_tests/config_test.clj | 9 --------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 6c04c82861..10a97b7bee 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -432,13 +432,6 @@ (or (c/get config :file-clean-delay) (ct/duration {:days 2}))) -(defn- join-path-segments - [segments] - (let [path (->> segments (map str) (str/join "/"))] - (->> (str/split path #"/") - (remove str/blank?) - (str/join "/")))) - (defn join-uri "Join path segments onto a base URI, preserving a potential subpath (same semantics as the frontend config). The base is normalized with @@ -447,8 +440,7 @@ [base & segments] (assert (not (some #(str/starts-with? % "/") segments)) "URI segments must be relative (no leading slash)") - (str (u/join (u/ensure-path-slash base) - (join-path-segments segments)))) + (str (apply u/join (u/ensure-path-slash base) segments))) (defn get-public-uri "Canonical public URI builder: `join-uri` over the configured diff --git a/backend/test/backend_tests/config_test.clj b/backend/test/backend_tests/config_test.clj index 3fb8a96e57..863be00a5e 100644 --- a/backend/test/backend_tests/config_test.clj +++ b/backend/test/backend_tests/config_test.clj @@ -34,15 +34,6 @@ (t/is (= "https://nitrate.example.com/api/teams/123" (cf/join-uri "https://nitrate.example.com/" "api/teams/123"))))) -(t/deftest join-uri-joins-multiple-segments - (t/testing "path segments are appended instead of replacing previous segments" - (t/is (= "https://nitrate.example.com/admin-console/api/teams/team-id/users/profile-id" - (cf/join-uri "https://nitrate.example.com/admin-console" - "api/teams/" - "team-id" - "users/" - "profile-id"))))) - (t/deftest join-uri-rejects-leading-slash (t/testing "a leading slash would drop the subpath, so it fails fast" (t/is (thrown? AssertionError From f962cf487c2bd41b19594ca7bcd0d21d5b447b0b Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Wed, 7 Oct 2026 11:17:40 +0200 Subject: [PATCH 045/145] :bug: Migrate legacy stroke width token references (#12018) Stroke width tokens used to be applied to the single `:stroke-width` token attribute. Applying tokens per side replaced it with the four `:stroke-width-*` attributes, but no migration moved the existing references, so files written before 2.19 look unapplied: the token pill stays off, the Design tab shows a plain number and propagation finds no update action. Add migration 0031, which copies the legacy reference to every side, keeps any side reference already present and drops the old key. It covers page and component shapes. Closes #12016 AI-assisted-by: deepseek-v4.1-flash --- common/src/app/common/files/migrations.cljc | 31 +++++- .../common_tests/files_migrations_test.cljc | 95 +++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index 649f980021..c0e97bc3d7 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -35,6 +35,7 @@ [app.common.types.shape.shadow :as ctss] [app.common.types.shape.text :as ctst] [app.common.types.text :as types.text] + [app.common.types.token :as ctt] [app.common.types.tokens-lib :as ctob] [app.common.types.variant :as ctv] [app.common.uuid :as uuid] @@ -2151,6 +2152,33 @@ (update :pages-index d/update-vals repair-container) (d/update-when :components d/update-vals repair-container)))) +(defmethod migrate-data "0031-migrate-stroke-width-token-attr" + ;; Stroke width tokens used to be applied to the single `:stroke-width` token + ;; attribute, replaced by the four per-side attributes. The editor reads only + ;; the new keys, so a file written before the change keeps the token on + ;; `:stroke-width` and the token looks unapplied even though its value is + ;; still set. Copy the legacy reference to every side (matching how applying + ;; the token resolves all sides) and drop the old key. A side that already + ;; carries a reference wins over the recycled global one. + [data _] + (letfn [(repair-shape [shape] + (let [applied (:applied-tokens shape) + token (:stroke-width applied)] + (if (some? token) + (update shape :applied-tokens + (fn [applied] + (-> (zipmap ctt/per-side-stroke-width-keys (repeat token)) + (merge applied) + (dissoc :stroke-width)))) + shape))) + + (repair-container [container] + (d/update-when container :objects d/update-vals repair-shape))] + + (-> data + (update :pages-index d/update-vals repair-container) + (d/update-when :components d/update-vals repair-container)))) + (def available-migrations (into (d/ordered-set) ["legacy-2" @@ -2238,4 +2266,5 @@ "0027-separate-tokens-status" "0028-normalize-constrained-values" "0029-move-background-blur-out-of-blur" - "0030-remove-stroke-per-side-attr"])) + "0030-remove-stroke-per-side-attr" + "0031-migrate-stroke-width-token-attr"])) diff --git a/common/test/common_tests/files_migrations_test.cljc b/common/test/common_tests/files_migrations_test.cljc index 7e92deda66..02c4a441da 100644 --- a/common/test/common_tests/files_migrations_test.cljc +++ b/common/test/common_tests/files_migrations_test.cljc @@ -8,8 +8,10 @@ (:require [app.common.data :as d] [app.common.files.migrations :as cfm] + [app.common.files.tokens :as cfo] [app.common.types.file :as ctf] [app.common.types.shape :as cts] + [app.common.types.token :as ctt] [app.common.uuid :as uuid] [clojure.test :as t])) @@ -459,3 +461,96 @@ (t/is (nil? (:stroke-per-side stroke')) "obsolete attr removed before schema validation") (t/is (= 5 (:stroke-width stroke')) "stroke width preserved"))) + +(t/deftest migration-0031-legacy-applied-token-is-not-recognized-before-migration + ;; Reproduces #12016: the editor reads only the per-side token attributes, so + ;; a shape carrying the old global `:stroke-width` reference looks unapplied. + (let [shape (-> (cts/setup-shape {:id (uuid/next) :type :rect}) + (assoc :applied-tokens {:stroke-width "30"}))] + (t/is (not (cfo/shapes-token-applied? {:name "30"} [shape] + ctt/stroke-width-keys)) + "a legacy applied token is invisible to the token lookup"))) + +(t/deftest migration-0031-recycles-global-stroke-width-token + (let [migration-id "0031-migrate-stroke-width-token-attr" + page-id (uuid/next) + shape-id (uuid/next) + shape (-> (cts/setup-shape {:id shape-id :type :rect}) + (assoc :applied-tokens {:stroke-width "30" + :fill "red"})) + data {:pages-index {page-id {:objects {shape-id shape}}}} + shape' (get-in (cfm/migrate-data data migration-id) + [:pages-index page-id :objects shape-id]) + applied' (:applied-tokens shape')] + + (t/is (nil? (:stroke-width applied')) "legacy key removed") + (t/is (= "30" (:stroke-width-top applied'))) + (t/is (= "30" (:stroke-width-right applied'))) + (t/is (= "30" (:stroke-width-bottom applied'))) + (t/is (= "30" (:stroke-width-left applied'))) + (t/is (= "red" (:fill applied')) "unrelated tokens preserved") + (t/is (cfo/shapes-token-applied? {:name "30"} [shape'] + ctt/stroke-width-keys) + "the token is recognized after migration"))) + +(t/deftest migration-0031-keeps-side-token-that-was-already-set + (let [migration-id "0031-migrate-stroke-width-token-attr" + page-id (uuid/next) + shape-id (uuid/next) + shape (-> (cts/setup-shape {:id shape-id :type :rect}) + (assoc :applied-tokens {:stroke-width "30" + :stroke-width-top "10"})) + data {:pages-index {page-id {:objects {shape-id shape}}}} + applied' (get-in (cfm/migrate-data data migration-id) + [:pages-index page-id :objects shape-id :applied-tokens])] + + (t/is (= "10" (:stroke-width-top applied')) + "an explicit side reference is not overwritten") + (t/is (= "30" (:stroke-width-right applied'))))) + +(t/deftest migration-0031-repairs-component-shapes + (let [migration-id "0031-migrate-stroke-width-token-attr" + component-id (uuid/next) + shape-id (uuid/next) + shape (-> (cts/setup-shape {:id shape-id :type :rect}) + (assoc :applied-tokens {:stroke-width "30"})) + data {:components {component-id {:objects {shape-id shape}}}} + applied' (get-in (cfm/migrate-data data migration-id) + [:components component-id :objects shape-id :applied-tokens])] + + (t/is (nil? (:stroke-width applied')) "legacy key removed") + (t/is (= "30" (:stroke-width-left applied')) + "component side reference created"))) + +(t/deftest migration-0031-leaves-shapes-without-the-legacy-token + (let [migration-id "0031-migrate-stroke-width-token-attr" + page-id (uuid/next) + shape-id (uuid/next) + shape (-> (cts/setup-shape {:id shape-id :type :rect}) + (assoc :applied-tokens {:stroke-width-top "30" + :stroke-width-right "30" + :stroke-width-bottom "30" + :stroke-width-left "30"})) + data {:pages-index {page-id {:objects {shape-id shape}}}}] + + (t/is (= data (cfm/migrate-data data migration-id)) + "a shape without the legacy token is left untouched"))) + +(t/deftest migration-0031-runs-through-file-migration + (let [migration-id "0031-migrate-stroke-width-token-attr" + shape-id (uuid/next) + file (ctf/make-file {:name "Legacy stroke width token"}) + page-id (first (get-in file [:data :pages])) + shape (-> (cts/setup-shape {:id shape-id :type :rect}) + (assoc :applied-tokens {:stroke-width "30"})) + file (-> file + (assoc :migrations (disj cfm/available-migrations migration-id)) + (assoc-in [:data :pages-index page-id :objects shape-id] shape)) + file' (cfm/migrate-file file {}) + applied' (get-in file' [:data :pages-index page-id :objects shape-id :applied-tokens])] + + (t/is (cfm/need-migration? file) "new migration detected") + (t/is (not (cfm/need-migration? file')) "new migration recorded") + (t/is (contains? (:migrations file') migration-id) "migration id persisted") + (t/is (= "30" (:stroke-width-top applied')) + "legacy token applied to the sides"))) From 051f01d559c772448adf7cb37b4f809bb12d3c34 Mon Sep 17 00:00:00 2001 From: Danny Shirely <69107616+danialshirali16@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:48:46 +0330 Subject: [PATCH 046/145] :bug: Keep per-side stroke widths out of inspect geometry (#12055) Remove per-side stroke widths from both Inspect geometry panels so Stroke is the only section that lists them. Keep dimensions, position, corner radii, and transforms in Size and position. Add rendered-component regressions for boards, rectangles, multiple selection, uniform strokes, and CSS declarations. Closes #12026 AI-assisted-by: GPT-6 Signed-off-by: Danial Shirali Signed-off-by: Eva Marco Co-authored-by: Eva Marco --- .../main/ui/inspect/attributes/geometry.cljs | 4 - .../ui/inspect/styles/panels/geometry.cljs | 5 - frontend/test/frontend_tests/runner.cljs | 2 + .../ui/inspect_geometry_test.cljs | 114 ++++++++++++++++++ 4 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 frontend/test/frontend_tests/ui/inspect_geometry_test.cljs diff --git a/frontend/src/app/main/ui/inspect/attributes/geometry.cljs b/frontend/src/app/main/ui/inspect/attributes/geometry.cljs index 4f99f5dba8..477a6f01b0 100644 --- a/frontend/src/app/main/ui/inspect/attributes/geometry.cljs +++ b/frontend/src/app/main/ui/inspect/attributes/geometry.cljs @@ -22,10 +22,6 @@ :left :top :border-radius - :border-block-start-width ; top - :border-inline-end-width ; right - :border-block-end-width ; bottom - :border-inline-start-width ; left :transform]) (mf/defc geometry-block diff --git a/frontend/src/app/main/ui/inspect/styles/panels/geometry.cljs b/frontend/src/app/main/ui/inspect/styles/panels/geometry.cljs index 53d95fb00e..b5dca89400 100644 --- a/frontend/src/app/main/ui/inspect/styles/panels/geometry.cljs +++ b/frontend/src/app/main/ui/inspect/styles/panels/geometry.cljs @@ -22,11 +22,6 @@ :border-start-end-radius :border-end-start-radius :border-end-end-radius - ;; side strokes - :border-block-start-width - :border-inline-end-width - :border-block-end-width - :border-inline-start-width :transform]) (def ^:private shape-prop->border-radius-prop diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index e45dd24bd4..501973b6e8 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -110,6 +110,7 @@ [frontend-tests.ui.ds-controls-numeric-input-test] [frontend-tests.ui.gradient-handlers-test] [frontend-tests.ui.history-test] + [frontend-tests.ui.inspect-geometry-test] [frontend-tests.ui.inspect-stroke-tokens-test] [frontend-tests.ui.layout-container-multiple-test] [frontend-tests.ui.measurements-test] @@ -251,6 +252,7 @@ 'frontend-tests.ui.ds-controls-numeric-input-test 'frontend-tests.ui.gradient-handlers-test 'frontend-tests.ui.history-test + 'frontend-tests.ui.inspect-geometry-test 'frontend-tests.ui.inspect-stroke-tokens-test 'frontend-tests.ui.layout-container-multiple-test 'frontend-tests.ui.measurements-test diff --git a/frontend/test/frontend_tests/ui/inspect_geometry_test.cljs b/frontend/test/frontend_tests/ui/inspect_geometry_test.cljs new file mode 100644 index 0000000000..3b9c5879d2 --- /dev/null +++ b/frontend/test/frontend_tests/ui/inspect_geometry_test.cljs @@ -0,0 +1,114 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.ui.inspect-geometry-test + (:require + ["react-dom/server" :as rds] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.main.ui.hooks :as hooks] + [app.main.ui.inspect.attributes.geometry :as geometry] + [app.main.ui.inspect.attributes.stroke :as stroke] + [app.main.ui.inspect.styles.panels.geometry :as styles-geometry] + [app.main.ui.inspect.styles.panels.stroke :as styles-stroke] + [app.util.code-gen.style-css :as css] + [cljs.test :as t :include-macros true] + [cuerdas.core :as str] + [frontend-tests.helpers.mock :as mock] + [rumext.v2 :as mf])) + +(def ^:private per-side-stroke + {:stroke-style :solid + :stroke-alignment :inner + :stroke-color "#000000" + :stroke-opacity 1 + :stroke-width 2 + :stroke-width-top 2 + :stroke-width-right 4 + :stroke-width-bottom 8 + :stroke-width-left 16}) + +(defn- shape + [type stroke] + {:id (uuid/next) + :type type + :parent-id uuid/zero + :frame-id uuid/zero + :selrect (grc/make-rect 0 0 120 80) + :points [(gpt/point 0 0) (gpt/point 120 0) + (gpt/point 120 80) (gpt/point 0 80)] + :transform (gmt/matrix) + :r1 5 :r2 5 :r3 5 :r4 5 + :strokes [stroke]}) + +(defn- render-panel + [panel shapes] + (rds/renderToStaticMarkup + (mf/element panel + #js {:shapes shapes + :objects (into {uuid/zero {:id uuid/zero + :type :frame + :selrect (grc/make-rect 0 0 120 80)}} + (map (juxt :id identity)) shapes) + :resolvedTokens {} + :colorSpace "hex" + :onGeometryShorthand identity + :onStrokeShorthand identity}))) + +(t/deftest geometry-panels-exclude-stroke-widths + (doseq [type [:frame :rect] + panel [geometry/geometry-panel* styles-geometry/geometry-panel*]] + (t/testing (str "geometry for " (name type)) + (let [markup (render-panel panel [(shape type per-side-stroke)])] + (t/is (not (re-find #"Border (block|inline).*? width" markup))) + (t/is (str/includes? markup "120px")) + (t/is (str/includes? markup "80px")) + (t/is (str/includes? markup "5px")))))) + +(t/deftest geometry-panels-exclude-stroke-widths-in-multiple-selection + (let [shapes [(shape :frame per-side-stroke) (shape :rect per-side-stroke)]] + (doseq [panel [geometry/geometry-panel* styles-geometry/geometry-panel*]] + (t/is (not (re-find #"Border (block|inline).*? width" + (render-panel panel shapes))))))) + +(t/deftest stroke-panels-keep-per-side-widths + (with-redefs [cf/flags (conj cf/flags :inspect-styles) + hooks/use-portal-container (mock/stub (fn [_] nil))] + (doseq [type [:frame :rect] + panel [stroke/stroke-panel* styles-stroke/stroke-panel*]] + (let [markup (render-panel panel [(shape type per-side-stroke)])] + (doseq [label ["Border block start width" "Border inline end width" + "Border block end width" "Border inline start width"]] + (t/is (str/includes? markup label))) + (doseq [value ["2px" "4px" "8px" "16px"]] + (t/is (str/includes? markup value))))))) + +(t/deftest code-view-keeps-each-per-side-width-once + (let [shape (shape :frame per-side-stroke) + code (css/get-shape-properties-css {(:id shape) shape} shape css/shape-css-properties)] + (doseq [declaration ["border-block-start-width: 2px;" + "border-inline-end-width: 4px;" + "border-block-end-width: 8px;" + "border-inline-start-width: 16px;"]] + (t/is (= 1 (count (re-seq (re-pattern declaration) code))))))) + +(t/deftest uniform-strokes-keep-width-in-the-stroke-panel + (let [uniform-stroke (-> per-side-stroke + (dissoc :stroke-width-top :stroke-width-right + :stroke-width-bottom :stroke-width-left) + (assoc :stroke-width 3)) + shapes [(shape :rect uniform-stroke)]] + (doseq [panel [geometry/geometry-panel* styles-geometry/geometry-panel*]] + (t/is (not (str/includes? (render-panel panel shapes) "Border width")))) + (with-redefs [cf/flags (conj cf/flags :inspect-styles) + hooks/use-portal-container (mock/stub (fn [_] nil))] + (doseq [panel [stroke/stroke-panel* styles-stroke/stroke-panel*]] + (let [markup (render-panel panel shapes)] + (t/is (str/includes? markup "Border width")) + (t/is (str/includes? markup "3px"))))))) From accea1abdecbc1c038714f6d77b7fe0d3850309d Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 7 Oct 2026 11:30:40 +0200 Subject: [PATCH 047/145] :zap: Enable wasm SIMD for Skia render-wasm builds (#12145) Link with -msimd128 and point SKIA_BINARIES_URL at the pdf+simd prebuilt archive. --- render-wasm/_build_env | 6 +++++- render-wasm/lint | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/render-wasm/_build_env b/render-wasm/_build_env index 2b653b185f..dc03bebd89 100644 --- a/render-wasm/_build_env +++ b/render-wasm/_build_env @@ -20,7 +20,7 @@ fi export BUILD_NAME="${BUILD_NAME:-render-wasm}" export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"}; export CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-"target/$RENDER_TARGET"}; -export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"} +export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp-pdf-simd.tar.gz"} # 256 MB of initial heap to perform less # initial calls to memory grow. @@ -39,8 +39,12 @@ export EM_MEMORY_GROWTH_GEOMETRIC_STEP="0.8" # Default: dlmalloc export EM_MALLOC="dlmalloc" +# -msimd128: allow wasm SIMD in the linked module. Skia prebuilts must be +# built with the same flag (penpot/skia-binaries, WASM_SIMD=1) for Skia/skcms +# kernels to actually contain v128 ops; engines do not autovec scalar wasm. export EMCC_CFLAGS="--no-entry \ --js-library src/js/wapi.js \ + -msimd128 \ -sMALLOC=$EM_MALLOC \ -sINVOKE_RUN=0 \ -sALLOW_TABLE_GROWTH=0 \ diff --git a/render-wasm/lint b/render-wasm/lint index 4ed7d6826d..0489e1040d 100755 --- a/render-wasm/lint +++ b/render-wasm/lint @@ -11,7 +11,7 @@ fi . ./_build_env frontend export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"}; -export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"} +export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp-pdf-simd.tar.gz"} ALLOWED_RULES="-D static_mut_refs" From 9abcfebf60c332d0940644b5ba7607cb1a799837 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 7 Oct 2026 11:52:48 +0200 Subject: [PATCH 048/145] :sparkles: Run binfile import and export as durable jobs (#12056) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Run binfile import and export as durable jobs Import and export of `.penpot` files become durable jobs: two commands create them and answer at once with a job id the caller follows, while a worker of the `binfile` queue does the work. Each job keeps its own ledger (owner, params frozen at creation, result, progress history and an expiry), and the legacy commands and their SSE keep working. Why: both operations ran inside the request. A large import held it open for minutes, progress only existed while the caller kept the stream open, and a client that went away left work nobody recorded, retried or could cancel. The unified `job` substrate already dispatched, leased, retried and logged events, but it had no user-facing ledger. How: - The substrate gains the pieces a user-facing job needs: optional job-def metadata (family, resource-role), an expiry on submit, the owner in the runner context, and job resources in their own bucket, written with the profile of the job and never deduplicated. - One generic command per family: the caller names the job and sends its business params, the registry checks the family, the job-def decodes and validates, and permission and quota are checked first. - Progress taps of the core become milestones of the job: a shared stage vocabulary with self-contained counters, throttled heartbeats, and an interrupt that stops the run inside its own transaction on cancel. - Cancel reaches pending and running jobs from both modals, and closing a modal mid-run cancels the job it was following. - The export re-checks the read permission at run time and answers with the package descriptor; the import re-checks the edition permission, runs the core in a transaction so a cancellation rolls it back, and releases the consumed package and its temporary copy. AI-assisted-by: deepseek-v4.1-flash * :recycle: Handle externally-cancelled binfile jobs end to end The `cancelled` outcome of a job nobody followed no longer hangs its screens: the data layers emit an explicit terminal message for it. - The export emits `{:cancelled true}` per file and the modal paints a neutral cancelled row instead of success; a message without an artifact never reaches the download branch. - The import closes every entry with a dedicated cancelled message through the existing error rendering, so the wizard completes. - Both flows are pinned by data-layer tests, including the red state they replace. Also in this change: the progress-contract comment of the management API, early business-params validation in create-import-job (before assembling the upload), a `release-resource` helper without fabricated job ids, a note on the heartbeat write cost, and a memory cleanup. AI-assisted-by: muse-spark-1.3-contributor * :recycle: Pre-translate the cancelled import message The cancelled entry of the import wizard carried its message as a key for the UI to translate, which needed a rehash marker comment. Like its neighboring errors, it now translates eagerly in the data layer: the literal call keeps rehash clean with no marker, and the tests assert through the same call instead of the raw key. AI-assisted-by: muse-spark-1.3-contributor * :recycle: Move cancelled strings under generic labels keys The dashboard-specific cancelled key never shipped: before use it moves under `labels.*` next to the generic strings both flows already use, with Spanish translations following the established wording. AI-assisted-by: muse-spark-1.3-contributor * :sparkles: Add minor improvements * :recycle: Rename export job command to create-export-binfile-job Export now has one command per job type, the shape the :export-assets command needs: create-export-binfile-job freezes exactly the :export-binfile job, so :name no longer travels in the body and only :params does. Import keeps the generic envelope. submit-job reads the queue from the job-def metadata ::jobs/queue-name (falling back to :binfile), read by the command and never by the substrate, so a job-def can route its work to another queue without a change in the command. The rename reaches the frontend and the tests: the binfile export is asked for under its own name, and the exporter service keeps create-export-job. AI-assisted-by: deepseek-v4.1-flash * :sparkles: Polish the import and export status rows The status row of a file already carries what the file is doing, so the generic "Uploading file…"/"Downloading file…" lines are gone: the import wizard drops its own (the export one went with the jobs refactor), along with the now-unused style. In the export, every file is queued up front, even the ones whose job is created only after the previous one is over, and a file stays queued until its first milestone: a worker picking the job up no longer ends the queue. The queued text is a step smaller than the milestone. AI-assisted-by: deepseek-v4.1-flash * :bug: Show the cancelled export and fix the import error The cancelled export was unreachable: mark-file-cancelled sets :loading false and the label lived inside the loading branch, so a cancelled file only painted a neutral notification. The terminal state now carries the label, and the export flags lose the `?` suffix (predicates keep it; a data prop is not a question). The import wizard rendered (:error entry) through tr, but every producer writes user-facing text (a hint or a translated label), never a key: it renders the text as-is now, and the dynamic-key ignores and the stale comment are gone. The total-text-limit comment no longer references the 200MiB it replaced; the value is 500MiB in develop already. AI-assisted-by: deepseek-v4.1-flash * :recycle: Reflect the real job params in the creation commands The creation commands declared their params as an open empty map, so the RPC contract and the generated docs said nothing about them. Each command now imports the schema from the job namespace (`export-binfile/schema:params`, `import-binfile/schema:create-params`), and the RPC validates the business params before the command runs. The job-def still owns the params and re-validates the assembled ones at run time. The commands no longer decode the params again: the RPC did it. Import joins export as one command per job type: `create-import-job` becomes `create-import-binfile-job`, its body no longer carries `:name` and the job-def is resolved by name like export's. Its creation schema keeps the version optional, because the command reads it from the package header when the caller does not say it, and fills the manifest metadata kept for audit. A version out of range is now refused by the schema (`:params-validation`) instead of the command's own `:unsupported-version` check, which is gone. AI-assisted-by: deepseek-v4.1-flash * :bug: Stop the test system from running job runners The test system is main/system-config plus main/worker-config with a few components dissoc'ed. The runners of default, webhook and cron were listed by hand; the binfile runner added with the durable binfile jobs was not, so a real penpot/job-runner/binfile/0 thread ran during the whole suite. That runner races the tests that drive the loop by hand (run-batch plus run-worker-loop): it takes the job the test just dispatched and completes it, and the test then reads a state it did not produce. That is the reported flake of binfile-jobs-integration-test, whose middle assertion waits for a runner of another queue to not take the job. It only reproduced in CI: in the devenv PENPOT_TENANT makes the tenant baked into main/worker-config differ from the one the test builds, so the two used different queue keys locally. Drop every [ :app.worker/runner] by key shape instead of listing the known queues, so a new queue cannot bring the race back. AI-assisted-by: deepseek-v4.1-flash * :sparkles: Upload the package one chunk at a time and as a percentage The upload milestone counts chunks, not units of work, so the row wrote "Uploading the package 3/9" over a file the user had uploaded once: it read as if nine packages were on their way. Write the stage as a percentage of the file instead ("Uploading file… (33%)"), and make the English text of the stage say file, not package. The chunks also go one at a time now (concat-all instead of merge-all with two in flight), so the percentage moves in a straight line instead of jumping. upload-blob-chunked is the generic uploader, so media and font uploads become sequential too. AI-assisted-by: deepseek-v4.1-flash * :globe_with_meridians: Complete the Spanish catalogue es.po was 25 keys short of en.po: the 15 that the durable binfile jobs introduced (14 jobs.progress.stage.* and labels.queued) plus 10 gaps that predate them (errors.connection-error, errors.save-retrying, the find shortcuts, workspace.header.retrying, workspace.tokens.stroke-width and the mcp session labels). sync -l es then sorts the entries, syncs the #: comments from en and drops the 7 dashboard.import.progress.* keys en no longer has. The msgid sets of en and es are now identical (0 missing, 0 extra). AI-assisted-by: deepseek-v4.1-flash * :recycle: Move the job status keys into the jobs namespace The three status words the durable import and export rows render were filed under labels.*, next to the generic labels bucket, while their progress stages live under jobs.progress.stage.*: the strings of one feature sat in two places. Rename them so the whole job vocabulary is one namespace: labels.queued -> jobs.queued labels.export-cancelled -> jobs.export-cancelled labels.import-cancelled -> jobs.import-cancelled No other locale has these keys yet, so the rename costs no translation. The convention (a key names the feature that renders it; labels.* holds the generic reusable words) is written down in the frontend/translations memory. AI-assisted-by: deepseek-v4.1-flash * :fire: Remove the translation keys nothing references Six keys of en.po (and of es.po, which now mirrors it) have no (tr ...) call site left in frontend/src or common/src, and no literal reference either: labels.uploading-file a duplicate of jobs.progress.stage.upload since the upload stage says "Uploading file…" labels.downloading-file dead since the durable jobs replaced the old import and export progress rows inspect.attributes.stroke.alignment.center inspect.attributes.stroke.alignment.inner inspect.attributes.stroke.alignment.outer workspace.toolbar.mcp-connect-here The first two are leftovers of the durable binfile jobs, the other four predate them. Both catalogues keep an identical msgid set afterwards. AI-assisted-by: deepseek-v4.1-flash --- .serena/memories/backend/core.md | 15 +- .serena/memories/backend/storage.md | 4 +- .serena/memories/backend/subtleties.md | 4 +- .serena/memories/backend/testing.md | 19 +- .serena/memories/clojure/design-rules.md | 1 + .../frontend/dashboard-viewer-subtleties.md | 1 + .../frontend/routing-app-shell-subtleties.md | 3 +- .serena/memories/frontend/translations.md | 11 +- .serena/memories/prod-infra/core.md | 4 +- backend/resources/climit.edn | 6 + backend/src/app/binfile/common.clj | 52 +- backend/src/app/binfile/jobs.clj | 171 +++++ backend/src/app/binfile/v3.clj | 247 +++--- backend/src/app/config.clj | 16 + backend/src/app/http/assets.clj | 30 +- backend/src/app/http/sse.clj | 15 +- backend/src/app/jobs.clj | 268 +++++-- backend/src/app/jobs/storage.clj | 152 ++++ backend/src/app/main.clj | 26 + backend/src/app/rpc.clj | 1 + backend/src/app/rpc/commands/binfile.clj | 39 +- backend/src/app/rpc/commands/jobs.clj | 355 +++++++++ backend/src/app/rpc/management/jobs.clj | 31 +- backend/src/app/rpc/quotes.clj | 56 ++ backend/src/app/storage.clj | 6 +- backend/src/app/storage/schema.clj | 3 +- backend/src/app/tasks/export_binfile.clj | 94 +++ backend/src/app/tasks/import_binfile.clj | 126 +++ backend/src/app/util/events.clj | 62 +- backend/src/app/worker/runner.clj | 30 +- .../test/backend_tests/binfile_jobs_test.clj | 403 ++++++++++ backend/test/backend_tests/config_test.clj | 20 +- backend/test/backend_tests/helpers.clj | 22 +- .../test/backend_tests/http_assets_test.clj | 42 + .../test/backend_tests/jobs_metrics_test.clj | 21 +- .../test/backend_tests/jobs_storage_test.clj | 205 +++++ backend/test/backend_tests/jobs_test.clj | 362 ++++++--- .../backend_tests/rpc/commands/jobs_test.clj | 717 ++++++++++++++++++ .../rpc_management_jobs_test.clj | 33 +- .../test/backend_tests/rpc_quotes_test.clj | 78 ++ backend/test/backend_tests/storage_test.clj | 40 + .../tasks/binfile_jobs_integration_test.clj | 268 +++++++ .../tasks/export_binfile_test.clj | 193 +++++ .../tasks/import_binfile_test.clj | 254 +++++++ .../test/backend_tests/util_events_test.clj | 77 ++ .../test/backend_tests/worker_runner_test.clj | 5 +- backend/test/backend_tests/worker_test.clj | 22 + frontend/src/app/main/data/exports/files.cljs | 84 +- frontend/src/app/main/data/imports.cljs | 294 +++++++ frontend/src/app/main/data/jobs.cljs | 94 +++ frontend/src/app/main/data/uploads.cljs | 33 +- frontend/src/app/main/repo.cljs | 1 + .../src/app/main/ui/dashboard/import.cljs | 183 ++--- .../src/app/main/ui/dashboard/import.scss | 9 +- frontend/src/app/main/ui/exports/files.cljs | 172 ++++- frontend/src/app/main/ui/jobs/progress.cljs | 69 ++ frontend/src/app/worker.cljs | 1 - frontend/src/app/worker/import.cljs | 253 ------ .../data/exports_files_test.cljs | 286 +++++++ .../frontend_tests/data/imports_test.cljs | 533 +++++++++++++ .../test/frontend_tests/data/jobs_test.cljs | 176 +++++ .../frontend_tests/data/uploads_test.cljs | 40 + frontend/test/frontend_tests/runner.cljs | 8 + .../frontend_tests/ui/jobs_progress_test.cljs | 44 ++ .../test/frontend_tests/util_zip_test.cljs | 31 +- frontend/translations/en.po | 128 ++-- frontend/translations/es.po | 170 +++-- 67 files changed, 6267 insertions(+), 952 deletions(-) create mode 100644 backend/src/app/binfile/jobs.clj create mode 100644 backend/src/app/jobs/storage.clj create mode 100644 backend/src/app/rpc/commands/jobs.clj create mode 100644 backend/src/app/tasks/export_binfile.clj create mode 100644 backend/src/app/tasks/import_binfile.clj create mode 100644 backend/test/backend_tests/binfile_jobs_test.clj create mode 100644 backend/test/backend_tests/jobs_storage_test.clj create mode 100644 backend/test/backend_tests/rpc/commands/jobs_test.clj create mode 100644 backend/test/backend_tests/tasks/binfile_jobs_integration_test.clj create mode 100644 backend/test/backend_tests/tasks/export_binfile_test.clj create mode 100644 backend/test/backend_tests/tasks/import_binfile_test.clj create mode 100644 backend/test/backend_tests/util_events_test.clj create mode 100644 frontend/src/app/main/data/imports.cljs create mode 100644 frontend/src/app/main/data/jobs.cljs create mode 100644 frontend/src/app/main/ui/jobs/progress.cljs delete mode 100644 frontend/src/app/worker/import.cljs create mode 100644 frontend/test/frontend_tests/data/exports_files_test.cljs create mode 100644 frontend/test/frontend_tests/data/imports_test.cljs create mode 100644 frontend/test/frontend_tests/data/jobs_test.cljs create mode 100644 frontend/test/frontend_tests/ui/jobs_progress_test.cljs diff --git a/.serena/memories/backend/core.md b/.serena/memories/backend/core.md index 287cd56dd2..1c4dc27119 100644 --- a/.serena/memories/backend/core.md +++ b/.serena/memories/backend/core.md @@ -57,10 +57,19 @@ For deeper details on transaction semantics, advisory locks, Transit vs JSON hel Every background job is a job-def: a plain `(defn execute-X [cfg params] ...)` in its namespace + a malli params schema + an `ig/init-key` that returns the job-def map `{::jobs/name, ::jobs/schema, ::jobs/handler, ::jobs/decoder, ::jobs/validator}` (decoder/validator precompiled at init). The registry is the `::jobs/defs` wiring in `app.main`, which also populates a module-level registry used by `jobs/submit` as fallback — job-def components cannot ig/ref `::jobs/defs` (wiring cycle). Submit from RPC code passes its RPC cfg (it carries `::jobs/defs` via ig/ref). - `::jobs/handler` is `(fn [context params] ...)`. It is NOT given the runner cfg: a job-def closes over its own dependencies, and handing it the runner cfg would let a handler reach the pool or any other component. A wrapper passes the context to its implementation only when the implementation takes it; most do not, and their implementation stays `[cfg params]`. -- The context is built by `jobs/make-context` and has exactly four keys: `id`, `name`, `label`, `resource-id`, behind a closed schema. `queue` is absent on purpose (a routing detail of the dispatcher, not a property of the job) and so are `retry-num`/`max-retries`/`attempt` (retry policy belongs to the runner, and an attempt number cannot be durable because the `noop` strategy re-runs without incrementing the counter). +- The context is built by `jobs/make-context` and has exactly five keys: `id`, `name`, `label`, `resource-id`, `profile-id`, behind a closed schema. `profile-id` is the owner of a user-facing job and nil for an internal one, and it is there so a handler can revalidate permissions and check who owns its resource without a second query. `queue` is absent on purpose (a routing detail of the dispatcher, not a property of the job) and so are `retry-num`/`max-retries`/`attempt` (retry policy belongs to the runner, and an attempt number cannot be durable because the `noop` strategy re-runs without incrementing the counter). - `jobs/invoke` is the in-process escape hatch: it delivers a nil context without a row, validates and delivers `::jobs/context` when given, and keeps `::jobs/job-id` independent. `::jobs/context` and `::jobs/job-id` are cfg options, not params. -- Business params live in the `params` column (plain JSON, decoded with the job-def decoder). `submit` takes an optional `::jobs/profile-id` and `::jobs/resource-id`, both validated as UUIDs and stored as columns; nothing is inferred from `params`. No caller passes a profile-id yet. -- `heartbeat` uses named options `[cfg & {:keys [job-id progress] :as options}]` and returns the number of durable writes. The job id falls back to `::jobs/job-id` on the cfg and then to the runner-bound `*job-id*`. A progress report needs `current`, allows optional `total` and `stage` (max 250 chars) and takes no other key. +- Business params live in the `params` column (plain JSON, decoded with the job-def decoder). `submit` takes optional `::jobs/profile-id`, `::jobs/resource-id` (both validated as UUIDs) and `::jobs/expires-at` (an instant: the retention the jobs GC sweeps by), all stored as columns; nothing is inferred from `params`. The binfile creation commands are the callers that pass a profile-id and a resource. +- A job-def may carry optional metadata keys read from the registry by its consumers, never by the substrate: `::jobs/family` (e.g. `:export`) groups the jobs of a family for quotes, `::jobs/resource-role` (`:input` or `:output`) says whether the job's `resource_id` is what it consumes or what it produces, and `::jobs/queue-name` names the queue the creation command routes the job to (`submit-job` falls back to `:binfile` when the def does not set it). +- Job resources (`app.jobs.storage`) are the objects a job owns through `job.resource_id`: they live in the `job-resource` bucket, are written with the profile of the job as owner (only that profile can read them back from `/assets/by-id`) and are never deduplicated. They carry no expiry of their own, because `expired-at` is stored as `deleted_at` and an object with it is unreadable from birth: the retention of the artifact is the retention of its job row. The writer marks the object as touched right away, so an artifact that no row references (a crash between the upload and the completion) is reclaimed by `storage-gc-touched`, and a row that does reference it freezes it until the jobs GC deletes the row and touches the object. +- The binfile jobs (`:export-binfile` and `:import-binfile`) run the core of `bf.v1`/`bf.v3` through `app.binfile.jobs`, which turns the `:progress` taps of the core into `progress` events of the job with `jobs/heartbeat` (the `modified_at` touch at most once per second, progress events at up to ~200ms). Each event is a self-contained milestone: a `:stage` keyword of one vocabulary shared by the two formats (the same work has a different name in v1 and v3) and an optional `:counters` map, with the counter of the activity named after its own stage and the outer scopes in course as context, which the tap carries itself (the reader puts `::outer-counters` in every tap inside them — today only `:files`, extendable to `:teams`/`:projects` for multi-scope exports — so the adapter remembers nothing between taps and a tap that happens outside any scope has no context); the ids and names the taps carry never reach the event. Every tap is reported, so a tap with no units to count still keeps the job alive, and a milestone the throttle drops loses nothing because the next one carries the same state. The adapter checks that the job is still active before the run, and the beat raises an `:interrupt` right there on the run thread when the job is no longer active, so the work stops at its next event point inside the transaction of its handler, which rolls back. A run whose last tap predates the interruption is discarded by the check once it returns. The adapter passes the job id from the handler context. Their job-def components carry pool, storage, metrics and msgbus, because a job with a `profile_id` cannot store an event without a msgbus on its cfg. Both readers answer with the same shape (`:file-ids` and, in version 3, the resolution of the libraries), because the version 1 reader returns a bare set of file ids. +- Both binfile handlers revalidate their permission at run time, not only when the job is created, because it can be revoked while the job waits: the export checks the read permission of every file before reading anything and stores the package in `job-resource` as the resource of the job (answering with the completion envelope), and the import checks the edition permission of the destination and runs the core inside a transaction, which is what makes a cancelled import roll back instead of leaving half of a file. The import releases its package (marks it as deleted and drops the reference from the job row in the same transaction) and deletes its temporary copy once the job is terminal, whatever the outcome. Both handlers check their own result against a schema before returning it, because the job-def has no result schema of its own. +- The domain cfg of a binfile request lives in `app.binfile.common` (`export-cfg`, `import-cfg`), so the legacy RPC and the jobs freeze the same ids, resolve the same team features and apply the same anti-zip-bomb limits. `resolve-export-type` is the translation the legacy RPC needs for its deprecated boolean flags; the creation command asks the caller for the type instead of guessing a default. +- Two retention knobs, easy to confuse: `:jobs-retention` sweeps terminal internal rows (the ones without a profile), while `:jobs-user-ttl` is the default expiry a user-facing job gets when it is created; the jobs GC deletes the row once `expires_at` passes and touches its resource. The heavy work of a user job runs on its own `:binfile` queue (`:worker-binfile-parallelism`, default 1) so it does not block the default runner. +- The quote of a family of jobs (`::export-jobs-per-profile`, `::import-jobs-per-profile`) counts the jobs of the profile that are still on their way (`new`/`scheduled`/`running`/`retry`) whose name the registry gives for that family, so a new job of the family is covered without touching the quote. +- A user-facing job is created through `app.rpc.commands.jobs`. One command per job type: `create-export-binfile-job` and `create-import-binfile-job` name the job they create and their body carries only `:params`, whose schema the command imports from the job namespace (`::sm/params`), so the RPC validates the business params up front, before the command runs; the job-def is still the place that owns them and validates them again at run time. The commands check the permission and the quote of the family before the expensive part, submit with `max-retries 0`, an expiry from `:jobs-user-ttl` and the queue the job-def routes to through `::jobs/queue-name` (`:binfile` when the def says nothing; read by the command, never by the substrate), and answer with a summary of the row. The import command has its own creation schema (`import-binfile/schema:create-params`, version optional) because the request is not the stored params: the command fills the version (from the caller or the package header) and the manifest metadata kept for audit. It assembles the upload, reads the manifest of a version 3 package for the audit (never its content) and stores the package in `job-resource` as the resource of the job, releasing it when the job never comes to own it. +- `get-job` is the read surface a profile has over its own jobs: it answers with the same summary the creation commands return plus the `result` of a finished job or its public error (through `decode-job-error`, so the keywords come back), and a job of another profile is not distinguishable from one that does not exist: both answer `not-found`. `decode-row` decodes `params`, `result` and `error`, and the result is the plain JSON the row holds, so a uuid comes back as the text it is on disk (a result has no schema of its own like the params do). The event log still has no read surface: a client that follows a job takes its progress from the websocket. +- `heartbeat` uses named options `[cfg & {:keys [job-id progress] :as options}]` and returns the number of durable writes, so 0 means there was nothing to write: no job context, or the throttle did not allow it. A write that finds the job no longer active (terminal or gone) raises an `:interrupt` (`code :job-interrupted`, carrying the job id and the status it found) through the shared `jobs/check-active` instead of counting zero, so a task in course stops at its next beat; the interrupt is internal and never reaches HTTP (the management progress report answers `:skip` for it). The job id falls back to `::jobs/job-id` on the cfg and then to the runner-bound `*job-id*`. The `modified_at` touch is throttled to ~1s and the progress event to ~200ms, so the check fires at most once per second with no extra query (the read of the row happens only when a write already found nothing). A progress report is a milestone (`schema:progress`): a required `:stage` keyword plus an optional `:counters` map of `scope -> {:current, :total?}`, and no other key. Counters are non-negative, a `total` is positive and never lower than its `current`, and stage and counter keys are names of at most 64 characters. A milestone without counters is valid (a point of the run with no units to count), the stage vocabulary belongs to the worker and not to the substrate, and a client must degrade for a stage it does not know. Read a stored payload back with `jobs/decode-progress`: the database holds the keywords as strings. The 1s beat (down from 60s, so a cancelled heavy job is seen as gone at its next beat) costs one `modified_at` touch per beating job per second: cheap while the only heavy beater runs alone on its queue, revisit if concurrent beating jobs ever grow. - `job.error` conforms to `app.jobs/schema:job-error` (`type` and `code` keywords, `hint` text, extra details allowed). Read it back with `jobs/decode-job-error`; a plain `db/decode-json-pgobject` leaves the keywords as strings. `complete` takes named options and an optional `resource-id`, which is only ever set, never replaced: it reads the row locked inside its own transaction and applies the rules in a fixed order, skipping a job that is no longer completable before refusing one for its resource, so the answer is a function of the row and not of the order two callers arrive in. For worker dispatch, cron, retry semantics (`ex/raise :type ::wrk/retry` with `:delay`/`:strategy`), deduplication, job events, and queue internals: `mem:backend/subtleties`. diff --git a/.serena/memories/backend/storage.md b/.serena/memories/backend/storage.md index baeba165a2..58cb134930 100644 --- a/.serena/memories/backend/storage.md +++ b/.serena/memories/backend/storage.md @@ -77,6 +77,7 @@ Since `put-object!` uses backend-specific operations (`impl/resolve-backend` + ` - Objects can therefore share content across users and files within one bucket. - Deleted objects are not reused. - `tempfile` objects never use deduplication, even when the caller requests it. +- `job-resource` objects never use deduplication either, whatever the caller asks: the object belongs to one profile, and a shared blob would serve the artifact of one user to the job of another. `put-object!` excludes the bucket from the dedupable set, so the invariant does not depend on call sites. - Use `sto/wrap-with-hash` when the caller already calculated the content hash. ## Bucket Rules @@ -94,7 +95,7 @@ Since `put-object!` uses backend-specific operations (`impl/resolve-backend` + ` | `file-data` | Encoded file data when `file-data-backend` is `storage`. Reference metadata has `storage-ref-id`, `file-id`, and the `file_data` row ID. | Yes | Authentication required | Reference scan. | | `file-data-fragment` | Compatibility value for file-data fragments. The current backend has no dedicated producer for this bucket. | No current write semantics | Public | No touched-object collector case. | | `file-change` | Compatibility value for file changes. Current snapshots store data in `file_data`, not this bucket. | No current write semantics | Authentication required | No touched-object collector case. | -| `job-resource` | Storage objects owned by job rows (`job.resource_id`). | Yes | Authentication required | jobs-GC touch → reference scan. | +| `job-resource` | Storage objects owned by job rows (`job.resource_id`), written by `app.jobs.storage`. | No | Authentication required, owner-scoped | jobs-GC touch → reference scan. | - The valid bucket set is `app.storage.schema/metadata-buckets` (derived from `bucket-requirements`, where a new bucket and its required metadata keys are one entry); `app.storage/valid-buckets` aliases it. - `file-media-object` is the default bucket for old rows without bucket metadata. @@ -108,6 +109,7 @@ Since `put-object!` uses backend-specific operations (`impl/resolve-backend` + ` - `app.http.assets` decides direct object authentication from the bucket. - Public buckets are `file-media-object`, `file-object-thumbnail`, `team-font-variant`, `file-data-fragment`, and `organization`. - Other valid buckets require a session or access-token profile ID. +- `tempfile` and `job-resource` are owner-scoped: the request must come from the profile stored in the object metadata, and a mismatch answers 404 (counted as `unauthorized`). A missing owner is tolerated only in `tempfile`, where legacy objects predate the metadata; `job-resource` always carries one, so an object without it is never served. - File-media routes also require file read permission. - Non-public direct responses set `content-disposition: attachment`. - FS responses use `x-accel-redirect` for the configured asset path. diff --git a/.serena/memories/backend/subtleties.md b/.serena/memories/backend/subtleties.md index 41f781070b..3e1b73a482 100644 --- a/.serena/memories/backend/subtleties.md +++ b/.serena/memories/backend/subtleties.md @@ -9,6 +9,7 @@ - Params with `::sm/params` are decoded/conformed through the JSON transformer and successful IObj results get `:encode/json` metadata. Legacy spec conforming only applies when no Malli params schema exists. Client params are stripped of qualified keys (`d/without-qualified`) before merging with the server auth context, so request bodies cannot override `::profile-id`, `::auth-type`, or `::token-perms`. - Params schemas are open by default, so undeclared client keys reach the handler unless the map is `:closed true`. Creation commands (`create-file`, `create-project`, `create-team`, `create-team-with-invitations`, `upload-file-media-object`, `create-file-media-object-from-url`, `assemble-file-media-object`) use closed schemas: a client-provided `:id` fails with `:params-validation`. Their internal creation functions still accept an optional explicit `:id` for imports, duplicates and deterministic test fixtures. - Nil RPC bodies become HTTP 204 unless explicit status metadata is present. Stream bodies default to `application/octet-stream` when no content type is set. +- The upload schema (`media.v/schema:upload`) takes a `:path` from the caller, and a Transit body can tag any string as a `path`, so a command that reads such an upload reads a file the client named. `create-import-binfile-job` avoids that boundary by accepting only a chunked upload (`:upload-id`); the commands that still take a plain file (the legacy `import-binfile`, the media and font uploads) trust it, and the guard belongs where uploads are accepted, not in each command. ## DB helpers @@ -25,7 +26,7 @@ - `job` carries `tenant` (the instance the job belongs to) and a bare `queue`. Several instances share one database and the tenant is what keeps an instance from claiming another environment's jobs, so every query that acts on one instance's work filters on `tenant`: dispatch claim, orphan sweep, lost-job reschedule, cron no-overlap count, submit dedupe. The `job__dispatcher__idx` leads with `tenant` (equality) then `(priority DESC, scheduled_at)`; `queue` does not lead because the dispatcher claims every queue of its tenant. The `:` prefix survives only in the Redis key, composed once by `app.worker/queue-key`. - Submit dedupe only removes not-yet-due `new` jobs of the same tenant with the same name/queue/label; it does not dedupe due, scheduled, retry, running, or completed work. - The dispatcher selects `new`/`retry` jobs with `FOR UPDATE SKIP LOCKED`, marks them `scheduled`, and publishes the JSON payload `[id scheduled-at]` to the `penpot.worker.queue::` Redis list. The runner skips Redis messages whose scheduled timestamp no longer matches DB state. -- Lost `scheduled` jobs are rescheduled after 5 minutes; `running` jobs untouched longer than `:jobs-lease` (default 30 min) are marked `aborted` with `app.jobs/orphan-error` (`type :internal`, `code :orphan`). `aborted` is a system-side terminal state like `cancelled` (never retried, swept by retention like other terminals, reported with an error log); long jobs must `heartbeat` per chunk so a healthy backlog is never mistaken for a dead process. The `:progress` report is optional and only feeds the durable log. +- Lost `scheduled` jobs are rescheduled after 5 minutes; `running` jobs untouched longer than `:jobs-lease` (default 30 min) are marked `aborted` with `app.jobs/orphan-error` (`type :internal`, `code :orphan`). `aborted` is a system-side terminal state like `cancelled` (never retried, swept by retention like other terminals, reported with an error log); long jobs must `heartbeat` per chunk so a healthy backlog is never mistaken for a dead process. The `:progress` report is optional and only feeds the durable log. A beat whose write finds the job no longer active raises an `:interrupt` (`code :job-interrupted`) instead of counting zero: it is the internal stop-the-work signal, not an error, so neither the listener nor the runner logs it as one. - An orphan gets its `end` event (`outcome "aborted"`) from the sweep itself: one bulk `INSERT ... SELECT` in the same transaction as the bulk `UPDATE`, never one insert per orphan. No msgbus notification goes out for it; orphans alert through the error log and the orphaned counter. - `job_event` is append-only and deleted by cascade with the job. `progress` is a row there, not a column: no Redis and no mutable column to keep in sync. - `job` is deletion-protected at the SQL level (`deletion_protection__tgr`): a stray `DELETE FROM job` raises instead of dropping the row and its `job_event` cascade without touching the referenced `storage_object`. The only intended deleters, the jobs GC and the submit dedupe, disable the guard with `SET LOCAL rules.deletion_protection TO off` inside their transaction; tests use `th/db-force-delete`. @@ -36,6 +37,7 @@ ## Config and HTTP/session middleware - `app.config/config` and `flags` are dynamic `defonce` vars populated from `PENPOT_*` env vars through the shared schema string transformer. Tests and tooling can bind them. +- Duration config values are parsed as ISO-8601 with the `PT` prefix, so a day is written in hours (`:jobs-retention` and `:jobs-user-ttl` default to a week, and an operator writes `168h`): `7d` does not parse, and the value fails validation at boot instead of being silently ignored. - `parse-flags` automatically adds `:disable-secure-session-cookies` when `public-uri` is plain HTTP and not localhost. This changes cookie defaults without an explicit env flag. - The backend sets Clojure `*assert*` globally from the `:backend-asserts` feature flag. Assertion-dependent checks can therefore differ by runtime flags. - Request body parsing is mostly POST-oriented and supports Transit JSON plus plain JSON. Plain JSON request keys are kebab-decoded before being merged into `:params`. diff --git a/.serena/memories/backend/testing.md b/.serena/memories/backend/testing.md index cacfbadfca..75c049e475 100644 --- a/.serena/memories/backend/testing.md +++ b/.serena/memories/backend/testing.md @@ -12,4 +12,21 @@ JVM `clojure.test` (kaocha runner) under `backend/test/backend_tests/`. - Multiple focus symbols are supported by REPEATING the flag (kaocha accumulates them, last-wins does not apply): `clojure -M:dev:test --focus backend-tests.ns-one --focus backend-tests.ns-two` — preferred over a shell loop of isolated runs. - Regression run: `clojure -M:dev:test` to ensure no regressions in related functional areas. - If you need to filter output, tee to a temp file first: `clojure -M:dev:test 2>&1 | tee /tmp/penpot-test-output.txt`. -- RPC test helpers `command!`/`management-command!` split the data map: qualified keys become server params, unqualified keys become request body params. To inject request-level context (headers, `:app.http/auth-key-id`, ip), pass a map under `:app.http/request` metadata; non-map `IRequest` stubs fall back to a dummy request. \ No newline at end of file +- RPC test helpers `command!`/`management-command!` split the data map: qualified keys become server params, unqualified keys become request body params. To inject request-level context (headers, `:app.http/auth-key-id`, ip), pass a map under `:app.http/request` metadata; non-map `IRequest` stubs fall back to a dummy request. +- The test system is `main/system-config` + `main/worker-config` with the + components that must not run in the background dropped: HTTP, the srepl, + cron, the dispatcher and **every** `[ :app.worker/runner]` + (`helpers/remove-job-runners`). A live runner of any queue races the tests + that drive the loop by hand (`wdisp/run-batch` + `@#'wrkr/run-worker-loop`): + it takes the job the test just dispatched and completes it behind the test's + back, so the test's own assertions read a state it did not produce. Do not + re-add a runner by editing that list by hand; the removal matches by key + shape on purpose. +- `system-config` and `worker-config` are `def`s: their `(cf/get ...)` calls + are evaluated when the namespace loads, against the real environment, while + test code calling `cf/get` at run time sees the `penpot-test`-prefixed + config. The two can disagree — in the devenv `:tenant` is `devenv-ws0` for + the defs and `default` inside a test — so a component built from + `worker-config` can talk to a different Redis key prefix than a cfg the test + builds itself. The symptom is a component that silently does nothing + locally and races in CI (which sets no `PENPOT_TENANT`). \ No newline at end of file diff --git a/.serena/memories/clojure/design-rules.md b/.serena/memories/clojure/design-rules.md index edf273e152..cac863cc16 100644 --- a/.serena/memories/clojure/design-rules.md +++ b/.serena/memories/clojure/design-rules.md @@ -6,6 +6,7 @@ How to shape a function in this codebase. Each rule came from a review round; do - **Optional-by-design data is guarded where the optionality is born.** Example: `ConnectorStatistics` is nil when the server option is off, so the guard is `when-let [cs (some-> server ...)]` at the entry point and the consumer assumes `cs`. - **A static set of operations is written statically.** With a fixed, small set (four metrics) write the four calls. Do not build a collection and iterate it (`doseq` over a literal vector): it allocates, adds indirection and hides each operation. Use a collection when the set is dynamic (config, registry, input). - **Default to no `!`; existing `!` is legacy being removed gradually.** Do not introduce new `!` names except the allowed cases documented in `mem:clojure/idioms` (add and justify the special case there first). Allowed: mutating public fns in `app.db` (`insert!`, `update!`, `delete!`, `run!`, `tx-run!`, ...) and fns whose contract is to work over transients (stdlib `conj!`/`assoc!`/`dissoc!`/`persistent!` and custom helpers over them). Existing `!` elsewhere (e.g. `emit!`, `send!`, `submit!`, `validate-file!`) stays until its refactor; keep a family consistent in the meantime. Reporting helpers (log, sample-and-publish) never take `!`: the mutation happens in the `!` API they call. +- **`?` marks a predicate, not a data prop.** A var or function that answers a question takes `?` (`export-error?`, `(defn editable? …)`); a map key that names a property does not (`:export-error`, never `:export-error?`), even when its value is a boolean. Existing `?` keys are legacy to remove gradually; do not add a new one. - **Do not shape production code for tests.** No return values added just to assert them; assert on the observable effect. When preconditions move, move the tests with them. Reference implementation: the http metrics samplers in `app.http` (`sample-worker-metrics` / `sample-connector-metrics` are guard-free; `sample-http-metrics` guards at the boundary). diff --git a/.serena/memories/frontend/dashboard-viewer-subtleties.md b/.serena/memories/frontend/dashboard-viewer-subtleties.md index f4cfd45cc9..097d3648fb 100644 --- a/.serena/memories/frontend/dashboard-viewer-subtleties.md +++ b/.serena/memories/frontend/dashboard-viewer-subtleties.md @@ -5,6 +5,7 @@ - Dashboard initialization fetches projects and fonts for the team, then listens to websocket messages only for global topic `uuid/zero` or the current profile id. - Project fetch replaces each project map completely instead of merging, so fields such as `deleted-at` can disappear cleanly. - Dashboard file/project mutations are often optimistic local updates with fire-and-forget RPC watchers. Bulk permanent delete/restore paths use SSE progress and progress notifications. +- The import wizard runs the package as a job of the backend: it reads the manifest of the file in the main thread (`app.main.data.imports`, no web worker) and follows the job, so its rows paint the milestone of the job (the stage and its counter, with the file in course) instead of the messages the old SSE stream sent. Before the job exists, the rows paint a client-side `:upload` milestone built from the chunked upload (`uploads/upload-blob-chunked` `:on-progress`, chunks done/total), which never comes from the backend; the chunks go one at a time and the row writes them as a percentage of the file, not as a counter. Once created and still waiting for a worker, each entry reports `:queued` ("Queued"); the `:start`/`:retry` events move it to bare progress, and only `:progress` carries a milestone. The export modal follows the same one-job-per-file model, but every file is queued up front (even the ones whose job is created only after the previous one is over) and its queue ends only with a milestone: `:start`/`:retry` do not end it there. Neither modal paints a generic "…ing file…" line any more: each row carries its own status. The `:error` of an import entry is user-facing text (the backend hint, or a translated label), never an i18n key: the wizard renders it as-is, it does not translate it again. The legacy SSE import stays in the backend for other clients. - File creation/duplication strips file `:data` before putting file summaries into dashboard state. ## Viewer diff --git a/.serena/memories/frontend/routing-app-shell-subtleties.md b/.serena/memories/frontend/routing-app-shell-subtleties.md index ea3e98243d..5c99aae4da 100644 --- a/.serena/memories/frontend/routing-app-shell-subtleties.md +++ b/.serena/memories/frontend/routing-app-shell-subtleties.md @@ -19,4 +19,5 @@ For general store mechanics such as `emit!`, `last-events`, persistence, and undo, read `mem:frontend/workspace-state-persistence-subtleties`. - Websocket initialization uses `cf/public-uri` joined with `ws/notifications`, converting `http/https` to `ws/wss`, and includes the current `session-id` as query param. -- Reinitializing or finalizing websocket stops the previous receive stream. Incoming websocket payloads become Potok data events under `app.main.data.websocket/message`. \ No newline at end of file +- Reinitializing or finalizing websocket stops the previous receive stream. Incoming websocket payloads become Potok data events under `app.main.data.websocket/message`. +- A job the client created is followed with `app.main.data.jobs/watch-job`: it emits the `:job-event` messages of that job (filtered by `:job-id`) and one final outcome read from the row with `get-job`, which is read when the stream starts and again on every `opened` of the websocket, so a job that ended before the client started following it, or while the socket was down, still answers. The stream ends after that outcome, and a read that fails reaches the caller: the repo retries the transient transport errors of a GET, and anything else is a real failure the flow has to show. The `:stage` of a progress event is a stable key of the vocabulary of the worker, never a display string: `app.main.ui.jobs.progress` resolves it with a `case` of literal `tr` calls, falls back to a generic label for a stage it does not know, and the counter of the activity is named after its own stage (the counter of `:files` is the outer context). \ No newline at end of file diff --git a/.serena/memories/frontend/translations.md b/.serena/memories/frontend/translations.md index 7053af9d4b..806ac5d990 100644 --- a/.serena/memories/frontend/translations.md +++ b/.serena/memories/frontend/translations.md @@ -7,10 +7,11 @@ high-coverage support reference, never the base. ## Workflow - Canonicalize with `node ./scripts/translations.js sync -l ` from `frontend/`: sorts entries, syncs `#:` comments from `en`, deletes keys missing in `en`. +- `sync` never ADDS a key missing in the locale: it only syncs comments (for keys that already exist) and drops keys absent from `en`. A key added to `en` alone stays missing in `es` and its text silently falls back to English. Every new key is born in `en` AND `es` in the same change; compare the `msgid` sets to check (`comm -23 <(grep '^msgid ' en.po | sort -u) <(grep '^msgid ' es.po | sort -u)` must be empty). - `sync` copies ALL comment flags from `en`, including `#, fuzzy`. Translated entries must NOT stay fuzzy: strip the flag after translating (mirror `es.po`, which keeps fuzzy only on genuinely untranslated entries). Fuzzy entries are excluded from `msgfmt --statistics` translated counts. - Canonical files carry NO `#~` obsolete blocks (`en`/`es` have zero). Drop them; `sync` does not resurrect them. -- Definition of done: `msgfmt --check .po` exit 0 and `msgfmt --statistics` shows 0 fuzzy, 0 untranslated. -- `rehash` scans `frontend/src` AND `common/src` for `(tr "key"` occurrences (any line counts, including `;;` comments) and refreshes `#:` refs in `en`, marking unreferenced keys `#, unused`. +- Definition of done: `msgfmt --check .po` exit 0, `msgfmt --statistics` shows 0 fuzzy, 0 untranslated, and the locale's `msgid` set is identical to `en.po`'s (no key missing, none extra). +- `rehash` scans `frontend/src` AND `common/src` for `(tr "key"` occurrences (any line counts, including `;;` comments) and refreshes `#:` refs in `en`, marking unreferenced keys `#, unused`. The refs in `en` are broadly stale, so a repo-wide `rehash` rewrites hundreds of lines (measured: ~487 on `en` alone): it is a maintenance action on its own, never a step to run inside a change that only adds or edits a key. - Dynamic keys are invisible to `rehash`: eliminate them instead of declaring them. Preference order: literal `(tr "key")` args only; never put a branch inside `tr`, hoist it out (`(if cond (tr "a") (tr "b"))`); @@ -28,6 +29,12 @@ high-coverage support reference, never the base. ## Entry rules +- Placement: a key names the feature that renders it (`dashboard.import.*`, + `files-export-modal.*`, `jobs.*`); `labels.*` is the bucket for the short + generic words reused across screens (Accept, Cancel, Download…), never the + home of a feature's own vocabulary. Keep a feature's strings together: the + durable jobs write their progress stages as `jobs.progress.stage.*` and + their row statuses as `jobs.*`, not half in `labels.*`. - Keys resolved dynamically from server-provided data carry a `#, backend` flag (set it in `en`, `sync` copies it to the locales): do not remove or rename these entries, and keep their `%s` placeholders (the backend only diff --git a/.serena/memories/prod-infra/core.md b/.serena/memories/prod-infra/core.md index d4999d86b8..7872efa128 100644 --- a/.serena/memories/prod-infra/core.md +++ b/.serena/memories/prod-infra/core.md @@ -12,9 +12,9 @@ Backend (`app.config`, `PENPOT_*` env vars) is parameterized; deployments choose ## Task queue and worker model -Async jobs are enqueued via `jobs/submit` (`app.jobs`), which inserts a row into the shared Postgres `job` table tagged with `queue = ":"`. Submission is **fire-and-forget** — RPC handlers never poll and never wait. The completion signal is the `job` row's `status` / `completed_at` columns, which nothing in `rpc/` reads today. A job submitted with `::jobs/profile-id` also publishes a `:job-event` message on that profile's msgbus topic after each transaction commits, but no caller passes a profile-id yet, so nothing reaches msgbus in production. Soft-delete RPCs return immediately after marking the top-level row, leaving the cascade and reaping to workers. +Async jobs are enqueued via `jobs/submit` (`app.jobs`), which inserts a row into the shared Postgres `job` table tagged with `queue = ":"`. Submission is **fire-and-forget** — RPC handlers never poll and never wait. The completion signal is the `job` row's `status` / `completed_at` columns, which the creation commands read back to answer with the state of the job they just created. A job submitted with `::jobs/profile-id` also publishes a `:job-event` message on that profile's msgbus topic after each transaction commits, and the binfile commands (`create-export-binfile-job`, `create-import-binfile-job`) are the callers that pass that owner, so the progress of their jobs does reach the msgbus. Soft-delete RPCs return immediately after marking the top-level row, leaving the cascade and reaping to workers. -Workers run on backends with `enable-backend-worker` in `PENPOT_FLAGS`. Each worker-enabled backend has a `dispatcher` (polls `job` with `FOR UPDATE SKIP LOCKED`, marks status='scheduled', RPUSHes the JSON `[id scheduled-at]` payloads into **its own** Redis list) and one or more `runner`s per queue (BLPOP from that same local list, execute, update the Postgres row). The Redis hand-off list is purely intra-backend — cross-backend coordination happens at the Postgres row level. +Workers run on backends with `enable-backend-worker` in `PENPOT_FLAGS`. Each worker-enabled backend has a `dispatcher` (polls `job` with `FOR UPDATE SKIP LOCKED`, marks status='scheduled', RPUSHes the JSON `[id scheduled-at]` payloads into **its own** Redis list) and one or more `runner`s per queue (BLPOP from that same local list, execute, update the Postgres row). The Redis hand-off list is purely intra-backend — cross-backend coordination happens at the Postgres row level. The queues are `default`, `webhooks`, `cron` and `binfile`: the last one carries the heavy work of a user job (`:worker-binfile-parallelism`, default 1), so a long export or import does not block the default runner. ## Cross-backend safety diff --git a/backend/resources/climit.edn b/backend/resources/climit.edn index c109b2afdb..1f59a0d3d3 100644 --- a/backend/resources/climit.edn +++ b/backend/resources/climit.edn @@ -53,6 +53,12 @@ :import-binfile/by-profile {:permits 1 :queue 2} + :create-import-binfile-job/global + {:permits 4} + + :create-import-binfile-job/by-profile + {:permits 1 :queue 2} + :profile-plugin-ops/global {:permits 4} diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index 2275a2cc6d..42085f7647 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -73,11 +73,11 @@ ;; Maximum total decompressed size allowed for all JSON/text zip entries ;; combined within a single import job: 500MiB. Bounds the case where many ;; entries, each individually under default-max-text-entry-size, still sum -;; to an unreasonable total. Set well above the 200MiB it replaces because -;; legitimate giant exports exist: hundreds of thousands of KB-sized -;; entries (pages, shapes) that never trip the per-entry cap but sum past -;; 400MiB. The per-entry cap still bounds single-entry DEFLATE -;; amplification, so this only moves the cumulative ceiling. +;; to an unreasonable total. Set this high because legitimate giant +;; exports exist: hundreds of thousands of KB-sized entries (pages, +;; shapes) that never trip the per-entry cap but sum past 400MiB. The +;; per-entry cap still bounds single-entry DEFLATE amplification, so this +;; only moves the cumulative ceiling. (def ^:const default-max-text-total-size (* 1024 1024 500)) @@ -1029,3 +1029,45 @@ {:bytes bytes :size (alength ^bytes bytes) :hash (sto/calculate-hash (java.io.ByteArrayInputStream. bytes))}))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; REQUEST CFG +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn resolve-export-type + "The canonical export type of a request on the legacy RPC: an explicit + `type` wins, and the deprecated boolean flags are translated. The + creation command of a job does not need it, because it asks the caller + for the type instead of guessing it." + [{:keys [type include-libraries embed-assets]}] + (cond + (some? type) type + (true? include-libraries) :include-libraries + (true? embed-assets) :merge-libraries + :else :detach-libraries)) + +(defn export-cfg + "The domain cfg of an export: the frozen set of files and how their + libraries are handled." + [cfg {:keys [ids export-type]}] + (-> cfg + (assoc ::ids ids) + (assoc ::export-type export-type))) + +(defn import-cfg + "The domain cfg of an import: the destination team/profile/project, the + name, the input and the anti-zip-bomb limits. The caller resolves the + team, because what allows it to import there differs between the RPC and + a job; what both must share is fixed here." + [cfg {:keys [profile-id project-id team name input]}] + (-> cfg + (assoc ::features (cfeat/get-team-enabled-features cf/flags team)) + (assoc ::project-id project-id) + (assoc ::profile-id profile-id) + (assoc ::team-id (:id team)) + (assoc ::name name) + (assoc ::input input) + (assoc ::import-max-binary-entry-size (cf/get :binfile-import-max-binary-entry-size)) + (assoc ::import-max-text-entry-size (cf/get :binfile-import-max-text-entry-size)) + (assoc ::import-max-text-total-size (cf/get :binfile-import-max-text-total-size)) + (assoc ::import-max-zip-entries (cf/get :binfile-import-max-zip-entries)))) diff --git a/backend/src/app/binfile/jobs.clj b/backend/src/app/binfile/jobs.clj new file mode 100644 index 0000000000..c947ae858a --- /dev/null +++ b/backend/src/app/binfile/jobs.clj @@ -0,0 +1,171 @@ +;; 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 SUBSIDIARY SL + +(ns app.binfile.jobs + "The binfile work a job runs. + + A job has no SSE channel, so the `:progress` points that `bf.v1` and + `bf.v3` emit become `progress` events of the job through + `jobs/heartbeat`, which throttles them and drops the ones that do not + fit. The adapter turns a tap into a self-contained milestone: a `:stage` + of a single vocabulary (the two formats name the same work differently) + and the counters the tap carries, with the outer scopes in course as + context. The reader puts the outer counters in every tap inside them, + so the adapter holds no state between taps. The ids the taps carry are + not part of the job event contract. + + The adapter checks that the job is still active before it starts, and + the beat checks it on every write: a job that was cancelled, or that + already reached a terminal state, raises an `:interrupt` right there on + the run thread, so the work stops at its next event point and the + transaction of its handler rolls back with it. A run whose last tap + happened before the interruption is discarded once it returns, which is + what makes that transaction roll back too." + (:require + [app.binfile.common :as bfc] + [app.binfile.v1 :as bf.v1] + [app.binfile.v3 :as bf.v3] + [app.common.exceptions :as ex] + [app.common.time :as ct] + [app.db :as db] + [app.jobs :as jobs] + [app.util.events :as events])) + +(defn check-active + "Raise an `:interrupt` when the job is no longer active. + + The substrate owns both the check and the error; this is the adapter's + entry point to it, used by the two handlers before their run and by the + tests." + [cfg context] + (jobs/check-active cfg (:id context))) + +(def ^:private stage-of + "The stage a tap section becomes: one vocabulary for the two formats, so + a client resolves each stage once. The counter of the activity is named + after its own stage, which is what lets a client render + `counters[stage]` without a table of equivalences." + {:file :files + :page :pages + :media :media + :thumbnail :thumbnails + :thumbnails :thumbnails + :component :components + :color :colors + :typography :typographies + :tokens-lib :tokens + :tokens-status :tokens + :storage-object :storage-objects + :storage-objects :storage-objects + :relations :relations + :manifest :manifest + :v1/metadata :metadata + :v1/files :files + :v1/rels :relations + :v1/sobjects :storage-objects}) + +(defn- stage + "The stage of a tap section; a section without a name of its own keeps + it, so a new tap is never dropped from the stream." + [section] + (get stage-of section section)) + +(defn- counter + "The counter a tap reports, when it reports one: a tap that only marks a + point of the run has no units to count." + [{:keys [current total]}] + (when (some? current) + (cond-> {:current current} + (some? total) + (assoc :total total)))) + +(defn- milestone + "The milestone a tap becomes: what the job is doing now and the counters + the tap carries, with the outer scopes in course as context. + + The tap of a scope carries its outer counters, so the milestone is + self-contained and the adapter remembers nothing between taps: a client + that misses one (the substrate throttles the events) still renders the + truth from the next." + [{:keys [section outer-counters] :as tap}] + (let [stage (stage section) + own (counter tap)] + (cond-> {:stage stage} + (or (some? outer-counters) (some? own)) + (assoc :counters (cond-> (or outer-counters {}) + (some? own) (assoc stage own)))))) + +(defn- with-progress + "Run `f` with the progress taps of the core turned into milestones of the + job. + + Every tap is reported: the substrate throttles the durable events, and + a milestone that does not fit loses no information because the next one + carries the same state. + + The taps run on this same thread: the sink calls the heartbeat inline, + so an `:interrupt` it raises aborts the core right there, inside the + transaction of its handler, which rolls back." + [cfg context f] + (let [job-id (:id context) + sink (fn [[type data]] + (when (= :progress type) + (jobs/heartbeat cfg + :job-id job-id + :progress (milestone data))))] + (binding [events/*sink* sink] + (let [result (f)] + (check-active cfg context) + result)))) + +(defn export-files + "Export a set of files into `output` as part of a job, reporting + progress as job events." + [cfg context {:keys [ids export-type output]}] + (check-active cfg context) + (with-progress cfg context + #(bf.v3/export-files! (bfc/export-cfg cfg {:ids ids + :export-type export-type}) + output))) + +(defn- touch-project + "Mark the destination project as modified. + + An import adds files, and the triggers of the schema only touch the + project on an update, so a project that just received files keeps its + old date and stays where it was in the list." + [cfg project-id] + (db/update! cfg :project + {:modified-at (ct/now)} + {:id project-id} + {::db/return-keys false})) + +(defn import-files + "Import the package at `input` as part of a job, reporting progress as + job events. The package was read from the resource of the job, so the + caller resolves the destination team and the adapter only fixes what + the legacy RPC and a job must share. + + Both formats answer with the same shape: the version 1 reader returns + the set of files it created, and the version 3 one returns a map that + already carries them together with the resolution of the libraries." + [cfg context {:keys [profile-id project-id team name input version]}] + (check-active cfg context) + (let [cfg (bfc/import-cfg cfg {:profile-id profile-id + :project-id project-id + :team team + :name name + :input input})] + (with-progress cfg context + #(let [result (case (int version) + 1 {:file-ids (vec (bf.v1/import-files! cfg))} + 3 (bf.v3/import-files! cfg) + (ex/raise :type :validation + :code :unsupported-version + :hint "unsupported binfile version" + :version version))] + (touch-project cfg project-id) + result)))) diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index 1d411336f7..338a4a8a6a 100644 --- a/backend/src/app/binfile/v3.clj +++ b/backend/src/app/binfile/v3.clj @@ -231,6 +231,32 @@ (def validate-file-thumbnail (sm/check-fn schema:file-thumbnail)) +;; --- PROGRESS + +(defn- tap-progress + "Emit a progress tap of the core, with the counters of the outer scopes + in course when the cfg has any. + + The outer counters live in `::outer-counters` of the cfg while a scope + is being processed (today only `:files`, tomorrow `:teams` and + `:projects` too), so every tap inside carries them as `:outer-counters`: + the stages inside do not repeat them, and whoever reads the taps (the + jobs adapter) builds its report from one tap alone instead of + remembering the last position it saw. A tap that happens outside any + scope carries no outer counters, which is the truth about it." + [cfg data] + (events/tap :progress + (cond-> data + (some? (::outer-counters cfg)) + (assoc :outer-counters (::outer-counters cfg))))) + +(defn- with-outer-counter + "Return cfg with the counter of `scope` set to `position`, keeping the + counters of the other outer scopes already in course, so entering a + nested scope never wipes the ones above it." + [cfg scope position] + (assoc-in cfg [::outer-counters scope] position)) + ;; --- EXPORT IMPL (defn- write-entry! @@ -264,8 +290,10 @@ (defn- export-storage-objects [{:keys [::output] :as cfg}] - (let [storage (sto/resolve cfg)] - (doseq [id (-> bfc/*state* deref :storage-objects not-empty)] + (let [storage (sto/resolve cfg) + ids (-> bfc/*state* deref :storage-objects not-empty) + total (count ids)] + (doseq [[index id] (d/enumerate ids)] (let [sobject (sto/get-object storage id) smeta (meta sobject) ext (cmedia/mtype->extension (:content-type smeta)) @@ -277,7 +305,8 @@ (write-entry! output path params) - (events/tap :progress {:section :storage-object :id id}) + (tap-progress cfg {:section :storage-object :id id + :current (inc index) :total total}) (with-open [input (sto/get-object-data storage sobject)] (.putNextEntry ^ZipOutputStream output (ZipEntry. (str "objects/" id ext))) @@ -305,7 +334,7 @@ thumbnails (bfc/get-file-object-thumbnails cfg file-id)] - (events/tap :progress {:section :file :id file-id :name (:name file)}) + (tap-progress cfg {:section :file :id file-id :name (:name file)}) (vswap! bfc/*state* update :files assoc file-id {:id file-id @@ -339,7 +368,8 @@ (write-entry! output path page) - (events/tap :progress {:section :page :id page-id :name (:name page) :file-id file-id}) + (tap-progress cfg {:section :page :id page-id :name (:name page) :file-id file-id + :current (inc index) :total (count pages)}) (doseq [[shape-id shape] objects] (let [path (str "files/" file-id "/pages/" page-id "/" shape-id ".json") @@ -350,30 +380,33 @@ (vswap! bfc/*state* bfc/collect-storage-objects media) (vswap! bfc/*state* bfc/collect-storage-objects thumbnails) - (doseq [{:keys [id] :as media} media] - (let [path (str "files/" file-id "/media/" id ".json") - media (encode-media media)] + (doseq [[index {:keys [id] :as item}] (d/enumerate media)] + (let [path (str "files/" file-id "/media/" id ".json") + encoded (encode-media item)] - (events/tap :progress {:section :media :id id :file-id file-id}) - (write-entry! output path media))) + (tap-progress cfg {:section :media :id id :file-id file-id + :current (inc index) :total (count media)}) + (write-entry! output path encoded))) - (doseq [thumbnail thumbnails] + (doseq [[index thumbnail] (d/enumerate thumbnails)] (let [data (cth/parse-object-id (:object-id thumbnail)) path (str "files/" file-id "/thumbnails/" (:tag data) "/" (:page-id data) "/" (:frame-id data) ".json") data (-> data (assoc :media-id (:media-id thumbnail)) (encode-file-thumbnail))] - (events/tap :progress {:section :thumbnails :id (:object-id thumbnail) :file-id file-id}) + (tap-progress cfg {:section :thumbnails :id (:object-id thumbnail) :file-id file-id + :current (inc index) :total (count thumbnails)}) (write-entry! output path data))) - (doseq [[id component] components] + (doseq [[index [id component]] (d/enumerate components)] (let [path (str "files/" file-id "/components/" id ".json") component (encode-component component)] - (events/tap :progress {:section :component :id id :file-id file-id}) + (tap-progress cfg {:section :component :id id :file-id file-id + :current (inc index) :total (count components)}) (write-entry! output path component))) - (doseq [[id color] colors] + (doseq [[index [id color]] (d/enumerate colors)] (let [path (str "files/" file-id "/colors/" id ".json") color (-> (encode-color color) (dissoc :file-id)) @@ -381,20 +414,22 @@ (and (contains? color :path) (str/empty? (:path color))) (dissoc :path))] - (events/tap :progress {:section :color :id id :file-id file-id}) + (tap-progress cfg {:section :color :id id :file-id file-id + :current (inc index) :total (count colors)}) (write-entry! output path color))) - (doseq [[id object] typographies] + (doseq [[index [id object]] (d/enumerate typographies)] (let [path (str "files/" file-id "/typographies/" id ".json") typography (encode-typography object)] - (events/tap :progress {:section :typography :id id :file-id file-id}) + (tap-progress cfg {:section :typography :id id :file-id file-id + :current (inc index) :total (count typographies)}) (write-entry! output path typography))) (when (and tokens-lib (not (ctob/empty-lib? tokens-lib))) (let [path (str "files/" file-id "/tokens.json") encoded-tokens (encode-tokens-lib tokens-lib)] - (events/tap :progress {:section :tokens-lib :file-id file-id}) + (tap-progress cfg {:section :tokens-lib :file-id file-id}) (write-entry! output path encoded-tokens))) (when tokens-status @@ -410,7 +445,7 @@ encoded-status (-> (encode-tokens-status tokens-status) (cfo/ids->names effective-tokens-lib))] - (events/tap :progress {:section :tokens-status :file-id file-id}) + (tap-progress cfg {:section :tokens-status :file-id file-id}) (write-entry! output path encoded-status))))) (defn- export-files @@ -449,7 +484,8 @@ (doseq [[index file-id] (d/enumerate ids)] (-> cfg (assoc ::file-id file-id) - (assoc ::file-seqn index) + (with-outer-counter :files {:current (inc index) + :total (count ids)}) (export-file))) ;; Write manifest file @@ -742,17 +778,19 @@ (defn- read-file-colors [{:keys [::bfc/input ::entries-index] :as cfg} file-id] - (->> (get-in entries-index [:colors (str file-id)]) - (reduce (fn [result {:keys [id entry]}] - (let [object (->> (read-entry cfg input entry) - (decode-color) - (validate-color))] - (events/tap :progress {:section :color :id id :file-id file-id}) - (if (= id (:id object)) - (assoc result id object) - result))) - {}) - (not-empty))) + (let [entries (get-in entries-index [:colors (str file-id)])] + (->> (d/enumerate entries) + (reduce (fn [result [index {:keys [id entry]}]] + (let [object (->> (read-entry cfg input entry) + (decode-color) + (validate-color))] + (tap-progress cfg {:section :color :id id :file-id file-id + :current (inc index) :total (count entries)}) + (if (= id (:id object)) + (assoc result id object) + result))) + {}) + (not-empty)))) (defn- read-file-components [{:keys [::bfc/input ::entries-index] :as cfg} file-id] @@ -771,15 +809,18 @@ (reduce-kv (fn [objects id shape] (assoc objects id (bfl/clean-shape-pre-decode shape))) objects - objects))))] + objects)))) + entries + (get-in entries-index [:components (str file-id)])] - (->> (get-in entries-index [:components (str file-id)]) - (reduce (fn [result {:keys [id entry]}] + (->> (d/enumerate entries) + (reduce (fn [result [index {:keys [id entry]}]] (let [object (->> (read-entry cfg input entry) (clean-component-pre-decode) (decode-component) (clean-component-post-decode))] - (events/tap :progress {:section :component :id id :file-id file-id}) + (tap-progress cfg {:section :component :id id :file-id file-id + :current (inc index) :total (count entries)}) (if (= id (:id object)) (assoc result id object) result))) @@ -788,22 +829,24 @@ (defn- read-file-typographies [{:keys [::bfc/input ::entries-index] :as cfg} file-id] - (->> (get-in entries-index [:typographies (str file-id)]) - (reduce (fn [result {:keys [id entry]}] - (let [object (->> (read-entry cfg input entry) - (decode-typography) - (validate-typography))] - (events/tap :progress {:section :typography :id id :file-id file-id}) - (if (= id (:id object)) - (assoc result id object) - result))) - {}) - (not-empty))) + (let [entries (get-in entries-index [:typographies (str file-id)])] + (->> (d/enumerate entries) + (reduce (fn [result [index {:keys [id entry]}]] + (let [object (->> (read-entry cfg input entry) + (decode-typography) + (validate-typography))] + (tap-progress cfg {:section :typography :id id :file-id file-id + :current (inc index) :total (count entries)}) + (if (= id (:id object)) + (assoc result id object) + result))) + {}) + (not-empty)))) (defn- read-file-tokens-lib [{:keys [::bfc/input ::entries-index] :as cfg} file-id] (when-let [{:keys [entry]} (first (get-in entries-index [:tokens (str file-id)]))] - (events/tap :progress {:section :tokens-lib :file-id file-id}) + (tap-progress cfg {:section :tokens-lib :file-id file-id}) (->> (read-entry cfg input entry nil) (decode-tokens-lib) (validate-tokens-lib)))) @@ -811,7 +854,7 @@ (defn- read-file-tokens-status [{:keys [::bfc/input ::entries-index] :as cfg} file-id tokens-lib] (when-let [{:keys [entry]} (first (get-in entries-index [:tokens-status (str file-id)]))] - (events/tap :progress {:section :tokens-status :file-id file-id}) + (tap-progress cfg {:section :tokens-status :file-id file-id}) (->> (read-entry cfg input entry nil) (cfo/names->ids tokens-lib) (decode-tokens-status) @@ -833,19 +876,21 @@ (defn- read-file-pages [{:keys [::bfc/input ::entries-index] :as cfg} file-id] - (->> (get-in entries-index [:pages (str file-id)]) - (keep (fn [{:keys [id entry]}] - (let [page (->> (read-entry cfg input entry) - (decode-page)) - page (dissoc page :options)] - (events/tap :progress {:section :page :id id :file-id file-id}) - (when (= id (:id page)) - (let [objects (read-file-shapes cfg file-id id)] - (assoc page :objects objects)))))) - (sort-by :index) - (reduce (fn [result {:keys [id] :as page}] - (assoc result id (dissoc page :index))) - (d/ordered-map)))) + (let [entries (get-in entries-index [:pages (str file-id)])] + (->> (d/enumerate entries) + (keep (fn [[index {:keys [id entry]}]] + (let [page (->> (read-entry cfg input entry) + (decode-page)) + page (dissoc page :options)] + (tap-progress cfg {:section :page :id id :file-id file-id + :current (inc index) :total (count entries)}) + (when (= id (:id page)) + (let [objects (read-file-shapes cfg file-id id)] + (assoc page :objects objects)))))) + (sort-by :index) + (reduce (fn [result {:keys [id] :as page}] + (assoc result id (dissoc page :index))) + (d/ordered-map))))) (defn- read-file-thumbnails [{:keys [::bfc/input ::entries-index] :as cfg} file-id] @@ -886,8 +931,11 @@ :plugin-data plugin-data})) (defn- import-file - [{:keys [::db/conn ::bfc/project-id ::manifest] :as cfg} {file-id :id file-name :name}] - (let [file-id' (bfc/lookup-index file-id) + [{:keys [::db/conn ::bfc/project-id ::manifest] :as cfg} + {file-id :id file-name :name} + position] + (let [cfg (with-outer-counter cfg :files position) + file-id' (bfc/lookup-index file-id) file (read-file cfg file-id) media (read-file-media cfg file-id) thumbnails (read-file-thumbnails cfg file-id)] @@ -899,16 +947,19 @@ :version (:version file) ::l/sync? true) + (tap-progress cfg {:section :file :file-id file-id}) + (vswap! bfc/*state* update :index bfc/update-index media :id) - (doseq [item media] + (doseq [[index item] (d/enumerate media)] (let [params (-> item (update :id bfc/lookup-index) (assoc :file-id file-id') (d/update-when :media-id bfc/lookup-index) (d/update-when :thumbnail-id bfc/lookup-index))] - (events/tap :progress {:section :media :id (:id params) :file-id file-id}) + (tap-progress cfg {:section :media :id (:id params) :file-id file-id + :current (inc index) :total (count media)}) (l/dbg :hint "inserting media object" :file-id (str file-id') @@ -921,7 +972,7 @@ (db/insert! conn :file-media-object params ::db/on-conflict-do-nothing? (::bfc/overwrite cfg)))) - (doseq [item thumbnails] + (doseq [[index item] (d/enumerate thumbnails)] (let [media-id (bfc/lookup-index (:media-id item)) object-id (-> (assoc item :file-id file-id') (cth/fmt-object-id)) @@ -935,13 +986,12 @@ :media-id (str media-id) ::l/sync? true) - (events/tap :progress {:section :thumbnail :file-id file-id :object-id object-id}) + (tap-progress cfg {:section :thumbnail :file-id file-id :object-id object-id + :current (inc index) :total (count thumbnails)}) (db/insert! conn :file-tagged-object-thumbnail params ::db/on-conflict-do-nothing? true))) - (events/tap :progress {:section :file :file-id file-id}) - (let [data (-> (read-file-data cfg file-id (:tokens-source file)) (d/without-nils) (assoc :id file-id') @@ -970,26 +1020,26 @@ (defn- import-file-relations [{:keys [::db/conn ::manifest ::bfc/timestamp] :as cfg}] - (events/tap :progress {:section :relations}) - (doseq [[file-id libr-id] (:relations manifest)] + (let [relations (:relations manifest)] + (doseq [[index [file-id libr-id]] (d/enumerate relations)] + (tap-progress cfg {:section :relations + :current (inc index) :total (count relations)}) - (let [file-id (bfc/lookup-index file-id) - libr-id (bfc/lookup-index libr-id)] + (let [file-id (bfc/lookup-index file-id) + libr-id (bfc/lookup-index libr-id)] - (when (and file-id libr-id) - (l/dbg :hint "create file library link" - :file-id (str file-id) - :lib-id (str libr-id) - ::l/sync? true) - (let [rel-params {:file-id file-id - :library-file-id libr-id}] - (db/insert! conn :file-library-rel rel-params) - (bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp))))))) + (when (and file-id libr-id) + (l/dbg :hint "create file library link" + :file-id (str file-id) + :lib-id (str libr-id) + ::l/sync? true) + (let [rel-params {:file-id file-id + :library-file-id libr-id}] + (db/insert! conn :file-library-rel rel-params) + (bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp)))))))) (defn- import-storage-objects [{:keys [::bfc/input ::entries-index ::bfc/timestamp] :as cfg}] - (events/tap :progress {:section :storage-objects}) - ;; IMPORTANT: we strongly do not reuse the main connection that can ;; run inside a transaction because the storage upload process can ;; fail in the middle of uploading and leave garbage on the underlying @@ -998,9 +1048,12 @@ ;; what the storage subsystem registers in other parallel ;; transaction (let [storage (sto/resolve cfg) - entries (:objects entries-index)] + entries (:objects entries-index) + total (count entries)] - (doseq [{:keys [id entry]} entries] + (doseq [[index {:keys [id entry]}] (d/enumerate entries)] + (tap-progress cfg {:section :storage-objects + :current (inc index) :total total}) (let [object (-> (read-entry cfg input entry) (decode-storage-object) (d/update-when :content-type bfc/normalize-content-type) @@ -1182,12 +1235,15 @@ (vswap! bfc/*state* update :index assoc old-lib-id library-id))) (let [files (get manifest :files) - file-ids (reduce (fn [result file] + total (count files) + file-ids (reduce (fn [result [index file]] (let [name' (get file :name) - file (assoc file :name name')] - (conj result (import-file cfg file)))) + file (assoc file :name name')] + (conj result (import-file cfg file + {:current (inc index) + :total total})))) [] - files) + (d/enumerate files)) ;; Build map of file-id to file-name for resolution files-info (into {} (map (fn [file-id manifest-file] [file-id (:name manifest-file)]) @@ -1244,7 +1300,7 @@ bfc/*reference-file* ref-file] (import-storage-objects cfg) - (import-file cfg file) + (import-file cfg file {:current 1 :total 1}) (invalidate-thumbnails cfg file-id) (bfm/apply-pending-migrations! cfg) @@ -1299,7 +1355,7 @@ (cfeat/check-supported-features! features))) - (events/tap :progress {:section :manifest}) + (tap-progress cfg {:section :manifest}) (binding [bfc/*state* (volatile! {:media [] :index {}})] (if (::bfc/file-id cfg) @@ -1352,6 +1408,11 @@ (vreset! ab true) nil) + (catch clojure.lang.ExceptionInfo cause + (if (= :interrupt (:type (ex-data cause))) + (vreset! ab true) + (throw cause))) + (catch Throwable cause (vreset! cs cause) (vreset! ab true) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 10a97b7bee..a4375c2184 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -79,6 +79,7 @@ :jobs-lease (ct/duration {:minutes 30}) :jobs-retention (ct/duration {:days 7}) + :jobs-user-ttl (ct/duration {:days 7}) :jobs-request-timeout (ct/duration {:minutes 2}) :media-max-file-size (* 1024 1024 30) ; 30MiB @@ -101,6 +102,8 @@ :quotes-upload-sessions-per-profile 5 :quotes-upload-chunks-per-session 20 + :quotes-export-jobs-per-profile 10 + :quotes-import-jobs-per-profile 10 :upload-max-chunk-size (* 1024 1024 30) ; 30MiB ;; SSRF protection @@ -196,6 +199,7 @@ [:file-clean-delay {:optional true} ::ct/duration] [:jobs-lease {:optional true} ::ct/duration] [:jobs-retention {:optional true} ::ct/duration] + [:jobs-user-ttl {:optional true} ::ct/duration] [:jobs-request-timeout {:optional true} ::ct/duration] [:telemetry-enabled {:optional true} ::sm/boolean] [:default-blob-version {:optional true} ::sm/int] @@ -215,6 +219,7 @@ [:worker-default-parallelism {:optional true} ::sm/int] [:worker-webhook-parallelism {:optional true} ::sm/int] [:worker-cron-parallelism {:optional true} ::sm/int] + [:worker-binfile-parallelism {:optional true} ::sm/int] [:database-password {:optional true} [:maybe :string]] [:database-uri {:optional true} ::sm/uri] @@ -233,6 +238,8 @@ [:quotes-font-variants-per-team {:optional true} ::sm/int] [:quotes-comment-threads-per-file {:optional true} ::sm/int] [:quotes-comments-per-file {:optional true} ::sm/int] + [:quotes-export-jobs-per-profile {:optional true} ::sm/int] + [:quotes-import-jobs-per-profile {:optional true} ::sm/int] [:quotes-snapshots-per-file {:optional true} ::sm/int] [:quotes-snapshots-per-team {:optional true} ::sm/int] [:quotes-team-access-requests-per-team {:optional true} ::sm/int] @@ -474,6 +481,15 @@ (or (c/get config :jobs-retention) (ct/duration {:days 7}))) +(defn get-jobs-user-ttl + "How long a user-facing job, and the artifact it owns, is kept before + the jobs GC deletes the row and marks the object for the storage GC. + Not the same as `:jobs-retention`, which only sweeps terminal internal + rows." + [] + (or (c/get config :jobs-user-ttl) + (ct/duration {:days 7}))) + (defn get "A configuration getter. Helps code be more testable." ([key] diff --git a/backend/src/app/http/assets.clj b/backend/src/app/http/assets.clj index e2c41d0886..e75f599fdc 100644 --- a/backend/src/app/http/assets.clj +++ b/backend/src/app/http/assets.clj @@ -150,14 +150,22 @@ [request] (some? (request-profile-id request))) -(defn- tempfile-owner-match? - "Check if the request's profile-id matches the tempfile's stored owner. - Returns true if no profile-id was stored (legacy objects)." +(def ^:private owner-scoped-buckets + "Buckets whose objects belong to a profile: a request from anybody else + must not reach them." + #{sto/tempfile-bucket sto/job-resource-bucket}) + +(defn- owner-match? + "Check that the request belongs to the profile that owns the object. + A missing owner is only tolerated in `tempfile`, where objects predate + the metadata; the `job-resource` bucket always carries one, so an + object without it fails closed." [obj request] - (let [stored-profile-id (:profile-id (meta obj)) - request-profile-id (request-profile-id request)] - (or (nil? stored-profile-id) - (= stored-profile-id request-profile-id)))) + (let [stored (-> obj meta :profile-id) + owner (request-profile-id request) + legacy? (= sto/tempfile-bucket (-> obj meta :bucket))] + (or (and legacy? (nil? stored)) + (= stored owner)))) (defn- serve-object-measured "Serve `obj`, recording one asset metric per outcome. A failure is @@ -175,7 +183,9 @@ "Handler that serves storage objects by id. For non-public buckets (e.g. profile), requires authentication via session cookie or access token. - For tempfile bucket, also requires ownership (profile-id match)." + For the owner-scoped buckets (tempfile, job-resource), also requires + ownership: the request must come from the profile the object belongs + to." [{:keys [::sto/storage] :as cfg} request] (let [id (get-id request) obj (sto/get-object storage id)] @@ -193,8 +203,8 @@ ;; The response stays 404 to avoid leaking existence, but the ;; metric records the internal 401 outcome. - (and (= (-> obj meta :bucket) sto/tempfile-bucket) - (not (tempfile-owner-match? obj request))) + (and (contains? owner-scoped-buckets (-> obj meta :bucket)) + (not (owner-match? obj request))) (do (emit-asset! cfg "by-id" obj 401) {::yres/status 404}) diff --git a/backend/src/app/http/sse.clj b/backend/src/app/http/sse.clj index 0fc7c385bd..4c1420876e 100644 --- a/backend/src/app/http/sse.clj +++ b/backend/src/app/http/sse.clj @@ -60,14 +60,13 @@ (partial write! output) (partial pu/close! output))] (try - (binding [events/*channel* channel] - (let [result (handler)] - (events/tap :end result))) - - (catch Throwable cause - (let [result (errors/handle' cause request)] - (events/tap channel :error result))) - + (binding [events/*sink* channel] + (try + (let [result (handler)] + (events/tap :end result)) + (catch Throwable cause + (let [result (errors/handle' cause request)] + (events/tap :error result))))) (finally (sp/close! channel) (px/await! listener))))))})) diff --git a/backend/src/app/jobs.clj b/backend/src/app/jobs.clj index 849ecb3784..be36f25549 100644 --- a/backend/src/app/jobs.clj +++ b/backend/src/app/jobs.clj @@ -9,9 +9,11 @@ Every job is defined as a job-def map returned by the `ig/init-key` of its module: {::name, ::schema, ::handler, ::decoder, ::validator} — decoder and - validator are precompiled at init time. The registry of job-defs is plain - integrant wiring (::jobs/defs); submit, dispatch and management consume it - by reference. + validator are precompiled at init time. A job-def may also carry the + optional metadata {::family, ::resource-role}, which the consumers of the + job (quotes, storage helpers) read from the registry: the substrate never + reads them. The registry of job-defs is plain integrant wiring (::jobs/defs); + submit, dispatch and management consume it by reference. Two execution modes are provided: - `submit` (durable): validates + JSON-encodes params and inserts a row @@ -27,8 +29,10 @@ on its loop, otherwise the dispatcher marks it `aborted` (a system-side terminal state, like `cancelled` but set by the dispatcher instead of a user: never retried, kept for triage and reported with an error - log) while its side effects continue. `heartbeat` also accepts an - optional `progress` report, which is stored as a `progress` row of + log) while its side effects continue. A beat that finds the job no + longer active raises an `:interrupt`, an internal signal that the task + should stop the work in course, never an error. `heartbeat` also accepts + an optional `progress` report, which is stored as a `progress` row of `job_event`: progress is durable history, not a mutable column, and needs no Redis. @@ -37,7 +41,7 @@ follow the job without polling the database. Reserved ledger columns (`profile_id`, `error`, `result`, `resource_id`, - `expires_at`) are only written by `submit` (profile and resource + `expires_at`) are only written by `submit` (profile, resource and expiry references) and by the terminal writers; `submit` never infers them from `params`." (:require @@ -82,14 +86,20 @@ whether to schedule again. A handler has no use for the counter, and the attempt number would lie anyway: the `noop` retry strategy runs again without incrementing it, so a durable \"which execution is this\" - number would need a second counter that nothing bounds." + number would need a second counter that nothing bounds. + + `profile-id` is the owner of a user-facing job and nil for an internal + one. It travels here so a handler can revalidate permissions and check + who owns the resource it is working on without a second query." [:map {:closed true :title "job-context"} [:id ::sm/uuid] [:name ::sm/text] [:label [:maybe ::sm/text]] ;; technical reference kept for garbage collection - [:resource-id [:maybe ::sm/uuid]]]) + [:resource-id [:maybe ::sm/uuid]] + ;; owner of a user-facing job; nil for an internal job + [:profile-id [:maybe ::sm/uuid]]]) (def check-context "Validate a handler context; raises with the malli explanation when it @@ -97,7 +107,7 @@ (sm/check-fn schema:context)) (defn make-context - "Build the handler context from a job row: exactly the four keys a + "Build the handler context from a job row: exactly the five keys a handler may see, and nothing else. The result is a plain map, not the database row, and it is not modified afterwards. @@ -107,7 +117,8 @@ (check-context {:id (:id job) :name (:name job) :label (if (str/blank? (:label job)) nil (:label job)) - :resource-id (:resource-id job)})) + :resource-id (:resource-id job) + :profile-id (:profile-id job)})) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; JOB DEFINITIONS (registry) @@ -123,6 +134,12 @@ (def ^:private schema:job-def [:map {:title "job-def"} [::name schema:job-name] + ;; Optional metadata of the job family, read from the registry by the + ;; consumers that need it (quotes, storage helpers, a future job + ;; listing). `app.jobs` itself never reads them: they are not part of + ;; the execution contract. + [::family {:optional true} ::sm/keyword] + [::resource-role {:optional true} ::sm/keyword] [::schema any?] ;; every handler is [context params]. A job-def already closes over its ;; own dependencies, so nothing else is handed to it: a handler cannot @@ -211,9 +228,9 @@ (def ^:private sql:insert-new-job "insert into job (id, name, tenant, params, queue, label, priority, - max_retries, profile_id, resource_id, created_at, - modified_at, scheduled_at) - values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + max_retries, profile_id, resource_id, expires_at, + created_at, modified_at, scheduled_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) returning id") ;; `tenant` is part of the match, not an extra: without it this DELETE @@ -243,7 +260,11 @@ [::profile-id {:optional true} ::sm/uuid] ;; Technical reference (storage object) kept for garbage collection. It ;; is a column, never part of `params`, and it is never inferred from it. - [::resource-id {:optional true} ::sm/uuid]]) + [::resource-id {:optional true} ::sm/uuid] + ;; Retention of the job: when it is set, the jobs GC deletes the row + ;; (and reclaims its resource) once the instant has passed. User-facing + ;; jobs set it at creation; internal jobs leave it nil. + [::expires-at {:optional true} ::ct/inst]]) (def check-options (sm/check-fn schema:options)) @@ -264,13 +285,17 @@ encodes them as plain JSON and inserts a row into the `job` table. Fire-and-forget: returns the job id immediately. + The optional ledger columns (`::profile-id`, `::resource-id` and + `::expires-at`) are validated and stored as columns; nothing is inferred + from `params`. + NOTE: the dedupe DELETE and the INSERT run atomically: joined to the caller's transaction when the cfg provides `::db/conn`, wrapped in their own transaction otherwise. Concurrent cross-backend submissions can, in rare race conditions, produce duplicated 'new' rows (accepted risk, see prod-infra documentation)." [cfg {:keys [::params ::name ::delay ::queue ::priority ::max-retries - ::dedupe ::label ::profile-id ::resource-id] + ::dedupe ::label ::profile-id ::resource-id ::expires-at] :or {delay 0 queue :default priority 100 max-retries 3 label ""} :as options}] @@ -322,7 +347,7 @@ :replace (or deleted 0)) (db/exec-one! conn [sql:insert-new-job id job-name tenant payload queue label priority max-retries - profile-id resource-id + profile-id resource-id expires-at now now scheduled-at])))] ;; Both statements always run inside db/tx-run!: joined to the ;; caller's transaction when the cfg provides a connection, @@ -348,7 +373,10 @@ (defn- decode-row [row] - (decode-json-col row :params)) + (-> row + (decode-json-col :params) + (decode-json-col :result) + (decode-json-col :error))) (defn get-job "Retrieve the job row (with the raw JSON params decoded to a plain map)." @@ -387,52 +415,93 @@ (def ^:private known-retry-reasons #{"backoff" "noop"}) -(def ^:private max-stage-length 250) +(def ^:private max-progress-key-length + "Longest accepted `:stage` or counter key. They name a vocabulary, they + are not a place for data, so the cap only keeps a runaway key out of the + log." + 64) + +(def schema:progress-counter + "Counter of one scope of a job: `current` is how many units are done and + `total` how many there are, when the worker knows it." + [:map {:closed true + :title "job-progress-counter"} + [:current ::sm/int] + [:total {:optional true} ::sm/int]]) (def schema:progress - "Progress report of a job: `current` is mandatory, `total` and `stage` are - optional, and no other key is accepted. `stage` is a short human label, - not a place for exception messages or params." + "Progress milestone of a job: the activity in course and the counters of + the scopes around it, each one under its own key. + + `:stage` is a stable keyword the client resolves (a translation, an + icon...), never a display string, and `:counters` is open on purpose: a + worker reports the scopes it knows and names the counter of the activity + after its own stage, so a client can render `counters[stage]` without a + table of equivalences. Neither is a place for names, params or exception + text: the payload is keywords and integers only." [:map {:closed true :title "job-progress"} - [:current ::sm/int] - [:total {:optional true} ::sm/int] - [:stage {:optional true} ::sm/text]]) + [:stage ::sm/keyword] + [:counters {:optional true} [:map-of ::sm/keyword schema:progress-counter]]]) (def check-progress - "Validate a progress report against its schema." + "Validate the shape of a progress milestone." (sm/check-fn schema:progress)) +(defn- check-counter + "Check the range rules of one counter: a non-negative `current` and a + positive `total` never lower than its `current`." + [key counter] + (when (neg? (:current counter)) + (ex/raise :type :validation + :code :invalid-progress + :hint "progress counter current must not be negative" + :counter key + :progress counter)) + (when-let [total (:total counter)] + (when (or (not (pos? total)) + (> (:current counter) total)) + (ex/raise :type :validation + :code :invalid-progress + :hint "progress counter total must be positive and not lower than current" + :counter key + :progress counter)))) + (defn validate-progress - "Check the range rules a schema cannot express: non-negative `current`, - positive `total`, `current` never greater than `total` and a short - `stage`." + "Check what the schema cannot express: non-negative counters, a positive + total never lower than its `current`, and stage and counter keys short + enough to be a name and not a payload." [progress] (let [progress (check-progress progress)] - (when (neg? (:current progress)) - (ex/raise :type :validation - :code :invalid-progress - :hint "progress current must not be negative" - :progress progress)) - (when-let [total (:total progress)] - (when (or (not (pos? total)) - (> (:current progress) total)) + (doseq [key (cons (:stage progress) (keys (:counters progress)))] + (when (> (count (name key)) max-progress-key-length) (ex/raise :type :validation :code :invalid-progress - :hint "progress total must be positive and not lower than current" + :hint (str "progress key must not be longer than " + max-progress-key-length " characters") + :key key :progress progress))) - (when (and (:stage progress) - (> (count (:stage progress)) max-stage-length)) - (ex/raise :type :validation - :code :invalid-progress - :hint (str "progress stage must not be longer than " - max-stage-length " characters") - :progress progress)) + (doseq [[key counter] (:counters progress)] + (check-counter key counter)) progress)) +(defn decode-progress + "Read back a stored progress payload as a Clojure map: `:stage` and the + counter keys come back as the strings the database holds, so they are + keywordized here, and a payload that is already a map is returned as is." + [progress] + (when (some? progress) + (-> (cond-> progress + (db/pgobject? progress) + (db/decode-json-pgobject)) + (d/update-when :stage keyword) + (d/update-when :counters + (fn [counters] + (into {} (map (fn [[k v]] [(keyword k) v])) counters)))))) + (def ^:private event-payload-keys {"start" #{:attempt} - "progress" #{:current :total :stage} + "progress" #{:stage :counters} "retry" #{:attempt :reason} "end" #{:outcome}}) @@ -490,7 +559,7 @@ :payload payload :created-at created-at})) -(defn insert-event +(defn- insert-event "Insert a `job_event` row, then count it and notify after the commit. Must be called inside a transaction that owns the job row. When the job @@ -501,7 +570,11 @@ The counter also runs after the commit, so a rolled back transaction never inflates it." [{:keys [::mbus/msgbus] :as cfg} job-id kind payload] - (let [payload (validate-event-payload kind payload) + (assert (db/connection-map? cfg) + "expected cfg with valid connection: insert-event must run inside a transaction") + (assert (some? db/*after-commit-context*) + "expected transaction context: insert-event must run inside a transaction") + (let [payload (validate-event-payload kind payload) connectable (db/get-connectable cfg) profile-id (:profile-id (db/exec-one! connectable [sql:job-event-owner job-id]))] (when (and profile-id @@ -543,8 +616,14 @@ ;; HEARTBEAT / PROGRESS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def ^:private heartbeat-interval (ct/duration {:seconds 60})) -(def ^:private progress-interval (ct/duration {:seconds 1})) +(def ^:private heartbeat-interval + ;; one second, down from a minute: a heavy job that was cancelled must + ;; be seen as gone at its next beat. The price is one `modified_at` + ;; touch per beating job per second (60x the old rate): cheap while the + ;; only heavy beater runs alone on its queue, revisit if concurrent + ;; beating jobs ever grow. + (ct/duration {:seconds 1})) +(def ^:private progress-interval (ct/duration {:millis 200})) (def ^:private prune-threshold 10000) (def ^:private prune-window (ct/duration {:hours 1})) @@ -597,6 +676,12 @@ (swap! heartbeats dissoc job-id) (swap! progresses dissoc job-id)) +(def ^:private active-statuses + "The statuses of a job that is still on its way. It mirrors the filter + of the heartbeat and progress writes below: a row outside this set is no + longer active and nothing can report to it." + #{"new" "scheduled" "running" "retry"}) + (def ^:private sql:touch-heartbeat "UPDATE job SET modified_at = ? @@ -632,6 +717,27 @@ 1) 0)))) +(defn check-active + "Raise an `:interrupt` when the job is no longer active. + + A job is active while its status is one of `active-statuses`; a row + that already reached a terminal state, or that no longer exists, is + not. The status is read from the row, because a write can only tell + that it matched no row, not why. + + The interrupt is an internal signal, not an error: the caller is a task + that must stop the work in course. `heartbeat` calls this when one of + its writes finds the job gone, and a caller can also ask explicitly, + which is what the binfile adapter does before and after its run." + [cfg job-id] + (let [job (get-job cfg job-id)] + (when-not (and (some? job) (contains? active-statuses (:status job))) + (ex/raise :type :interrupt + :code :job-interrupted + :hint "the job is no longer active" + :job-id job-id + :status (or (:status job) "gone"))))) + (defn heartbeat "Keep a running job alive and, optionally, report its progress. @@ -642,14 +748,18 @@ - `:progress` optional progress report (see `schema:progress`). - `::force?` internal: skips only the progress throttle. - Returns the number of durable writes performed, so 0 means the job is - terminal, has no job context, or the throttle did not allow a write. + Returns the number of durable writes performed, so 0 means there was + nothing to write: no job context, or the throttle did not allow it. A + write that finds the job no longer active raises an `:interrupt` (see + `check-active`) instead of counting zero, so a task in course stops at + its next beat. - The `modified_at` touch is throttled to ~60s and the progress event to - ~1s: external workers report progress as sparse milestones, while - handlers may beat on every iteration. Both writes go through the - connection pool (`::db/pool` on the cfg), never the caller transaction, - so a beat survives a rollback of the surrounding work. + Both writes go through the connection pool (`::db/pool` on the cfg), + never the caller transaction, so a beat survives a rollback of the + surrounding work: the `modified_at` touch is throttled to ~1s while + progress events go out at up to ~200ms, so the user sees movement + without hammering the row. External workers report progress as sparse + milestones, while handlers may beat on every iteration. Like every other function here the cfg is a cfg: a bare pool or connection is a caller bug and is not accommodated. @@ -662,7 +772,7 @@ forced path used by the management API propagates it so the external worker can retry." [cfg & {:keys [job-id progress] :as options}] - (let [job-id (or job-id (get cfg ::job-id) *job-id*) + (let [job-id (or job-id (get cfg ::job-id) *job-id*) ;; a malformed report is a caller bug: never swallow it progress (some-> progress validate-progress)] (when (uuid? job-id) @@ -671,20 +781,31 @@ force? (boolean (::force? options)) writes (volatile! 0)] (when (should-write? heartbeats job-id now heartbeat-interval) - (vswap! writes + (touch-job connectable job-id now))) + (let [n (touch-job connectable job-id now)] + ;; the touch is conditional on the row still being active: a + ;; write that matched no row is what raises the interrupt + (when (zero? n) + (check-active cfg job-id)) + (vswap! writes + n))) (when (and (some? progress) (or force? (should-write? progresses job-id now progress-interval))) - (let [store #(report-progress cfg job-id progress)] - (if force? - (vswap! writes + (store)) - (try - (vswap! writes + (store)) - (catch Throwable cause - (l/err :hint "unable to persist job progress" - :tenant (cf/get :tenant) - :job-id (str job-id) - :cause cause)))))) + (let [store #(report-progress cfg job-id progress) + n (if force? + (store) + (try + (store) + (catch Throwable cause + (l/err :hint "unable to persist job progress" + :tenant (cf/get :tenant) + :job-id (str job-id) + :cause cause) + 0)))] + ;; the raise stays outside the catch above: an interrupt is + ;; not a transient failure and must not be swallowed with it + (when (zero? n) + (check-active cfg job-id)) + (vswap! writes + n))) (int @writes))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -976,14 +1097,17 @@ "UPDATE job SET status='cancelled', modified_at=? WHERE id=? - AND status IN ('new','scheduled','retry') + AND status IN ('new','scheduled','retry','running') RETURNING id") (defn cancel - "Cancel a pending job (new/scheduled/retry) and record the `end` event. + "Cancel a pending or running job (new/scheduled/retry/running) and + record the `end` event. - A running or terminal job is left untouched and writes no event. Returns - the number of affected rows." + A running job stops at its next heartbeat: the beat finds no active + row and raises an `:interrupt`, which the runner treats as a normal + stop while the handler rolls back. A terminal job is left untouched + and writes no event. Returns the number of affected rows." [cfg job-id] (assert (mtx/instance cfg) "missing ::mtx/metrics on the jobs cfg") (let [job (get-job cfg job-id) diff --git a/backend/src/app/jobs/storage.clj b/backend/src/app/jobs/storage.clj new file mode 100644 index 0000000000..dd33252cb7 --- /dev/null +++ b/backend/src/app/jobs/storage.clj @@ -0,0 +1,152 @@ +;; 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 SUBSIDIARY SL + +(ns app.jobs.storage + "Storage helpers for the objects a job owns through `job.resource_id`. + + A job resource lives in the `job-resource` bucket and belongs to the + profile that owns the job, so only that profile can read it back (the + asset route checks the owner, not just the session). + + The job row is what keeps the object alive: `storage-gc-touched` + freezes it while a row references it and deletes it once no row does, + and the jobs GC marks it as touched when it removes the row. The object + carries no expiry of its own: `expired-at` is stored as `deleted_at`, + which makes the object unreadable from birth, so the retention of the + artifact is the retention of its job." + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.time :as ct] + [app.config :as cf] + [app.db :as db] + [app.storage :as sto] + [app.storage.tmp :as tmp])) + +(defn- resource-uri + "The url a user downloads the artifact from. A string, because the + descriptor ends up in the JSON result of the job." + [id] + (cf/get-public-uri "assets/by-id/" (str id))) + +(defn put-resource + "Store an artifact of a job in the `job-resource` bucket and return the + descriptor that identifies it. + + Named options: + + - `:content` the artifact, an `app.storage/content` value built from a + temporary file or from bytes in memory + - `:filename` the name the user will see + - `:mtype` the content type of the artifact + + `profile-id` is the owner of the object: the profile that owns the job, + for both the package a job consumes and the artifact it produces. + + The object is marked as touched right away, so an artifact that no job + row ever references (a crash between the upload and the completion) is + reclaimed by the storage GC instead of leaking; while a row references + it, `storage-gc-touched` freezes it." + [cfg profile-id {:keys [content filename mtype]}] + (assert (uuid? profile-id) "expected the profile that owns the resource") + (assert (string? mtype) "expected the content type of the resource") + (let [storage (sto/resolve cfg) + object (sto/put-object! storage + {::sto/content content + ::sto/touched-at (ct/now) + :bucket sto/job-resource-bucket + :profile-id profile-id + :content-type mtype})] + {:resource-id (:id object) + :resource-uri (resource-uri (:id object)) + :filename filename + :mtype mtype + :size (:size object)})) + +(defn- check-input + "The object a job consumes must exist, be a job resource and belong to + the profile that owns the job. Raises `:not-found` otherwise: a handler + never reads another profile's package." + [context object] + (when (or (nil? object) + (not= sto/job-resource-bucket (:bucket (meta object))) + (not= (:profile-id context) (:profile-id (meta object)))) + (ex/raise :type :not-found + :code :job-resource-not-found + :hint "the input of the job is not available" + :job-id (:id context) + :resource-id (:resource-id context))) + object) + +(defn load-input + "Download the package a job consumes to a local temporary file and + return its path, ready for the binfile reader. + + An object that was released or expired is already invisible to + `sto/get-object`, so it lands in the same `:not-found` as a missing + one." + [cfg context] + (let [storage (sto/resolve cfg) + resource-id (:resource-id context) + object (check-input context + (when (uuid? resource-id) + (sto/get-object storage resource-id)))] + (if-let [input (sto/get-object-data storage object)] + (tmp/tempfile-from input) + (ex/raise :type :not-found + :code :job-resource-not-found + :hint "the input of the job is not readable" + :job-id (:id context) + :resource-id resource-id)))) + +(defn release-resource + "Release a job resource. + + The object is marked as deleted and, when it belongs to a job, the + row stops pointing at it in the same transaction: a row never + references an object that is already gone, and the foreign key is not + what ends up clearing the reference. A resource no job owns (the + package of a creation that never submitted) is released with a nil + job id, and then no row is touched at all. + + Best effort: a failure is logged and never changes the terminal state + of the job, and releasing without a resource is a no-op. Returns true + when the object was marked as deleted or a stale reference was + cleared." + ([cfg resource-id] + (release-resource cfg resource-id nil)) + ([cfg resource-id job-id] + (when (uuid? resource-id) + (try + (db/tx-run! + cfg + (fn [tx-cfg] + (let [storage (sto/resolve tx-cfg ::db/reuse-conn true) + deleted? (sto/del-object! storage resource-id) + ;; the job no longer owns the package: the pointer goes with + ;; it, so clearing it does not depend on the object being + ;; physically deleted later + cleared? (when (some? job-id) + (db/update! tx-cfg :job + {:resource-id nil :modified-at (ct/now)} + ["id = ? AND resource_id = ?" + job-id resource-id] + {::db/return-keys [:id]}))] + (or deleted? (some? cleared?))))) + (catch Throwable cause + (l/wrn :hint "unable to release the input of a job" + :job-id (str job-id) + :resource-id (str resource-id) + :cause cause) + false))))) + +(defn release-input + "Release the package a job consumed. + + Delegates to `release-resource` with the id of the job: the object + and the pointer are dropped together. Best effort, like there." + [cfg context] + (release-resource cfg (:resource-id context) (:id context))) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 419dd88175..87d0b31f29 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -590,6 +590,8 @@ :file-gc (ig/ref :app.tasks.file-gc/job-def) :offload-file-data (ig/ref :app.tasks.offload-file-data/job-def) :objects-gc (ig/ref :app.tasks.objects-gc/job-def) + :export-binfile (ig/ref :app.tasks.export-binfile/job-def) + :import-binfile (ig/ref :app.tasks.import-binfile/job-def) :storage-gc-deleted (ig/ref ::sto.gc-deleted/job-def) :storage-gc-touched (ig/ref ::sto.gc-touched/job-def) :storage-pending-gc (ig/ref ::sto.pending-gc/job-def) @@ -642,6 +644,19 @@ ::sto/storage (ig/ref ::sto/storage) ::mtx/metrics (ig/ref ::mtx/metrics)} + :app.tasks.export-binfile/job-def + {::db/pool (ig/ref ::db/pool) + ::sto/storage (ig/ref ::sto/storage) + ::mtx/metrics (ig/ref ::mtx/metrics) + ;; the job belongs to a profile, so its events are published + ::mbus/msgbus (ig/ref ::mbus/msgbus)} + + :app.tasks.import-binfile/job-def + {::db/pool (ig/ref ::db/pool) + ::sto/storage (ig/ref ::sto/storage) + ::mtx/metrics (ig/ref ::mtx/metrics) + ::mbus/msgbus (ig/ref ::mbus/msgbus)} + :app.storage.gc-deleted/job-def {::db/pool (ig/ref ::db/pool) ::sto/storage (ig/ref ::sto/storage) @@ -840,6 +855,17 @@ ::db/pool (ig/ref ::db/pool) ::mbus/msgbus (ig/ref ::mbus/msgbus)} + ;; the heavy work of a user job does not share the default runner + [::binfile ::wrk/runner] + {::wrk/parallelism (cf/get :worker-binfile-parallelism 1) + ::wrk/queue :binfile + ::wrk/tenant (cf/get :tenant) + ::rds/client (ig/ref ::rds/client) + ::jobs/defs (ig/ref ::jobs/defs) + ::mtx/metrics (ig/ref ::mtx/metrics) + ::db/pool (ig/ref ::db/pool) + ::mbus/msgbus (ig/ref ::mbus/msgbus)} + [::cron ::wrk/runner] {::wrk/parallelism (cf/get :worker-cron-parallelism 2) ::wrk/queue :cron diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index cf80790b2e..d1b5247b95 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -379,6 +379,7 @@ 'app.rpc.commands.files-update 'app.rpc.commands.files-snapshot 'app.rpc.commands.files-thumbnails + 'app.rpc.commands.jobs 'app.rpc.commands.ldap 'app.rpc.commands.management 'app.rpc.commands.media diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index de0d3faadd..67163c1568 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -10,7 +10,6 @@ [app.binfile.common :as bfc] [app.binfile.v1 :as bf.v1] [app.binfile.v3 :as bf.v3] - [app.common.features :as cfeat] [app.common.schema :as sm] [app.common.time :as ct] [app.common.uri :as u] @@ -47,19 +46,12 @@ [:embed-assets {:optional true} ::sm/boolean]]) (defn- export-binfile - [{:keys [::sto/storage] :as cfg} {:keys [type file-id include-libraries embed-assets]}] - (let [output (tmp/tempfile*) - ;; Convert legacy boolean flags to unified export-type - export-type (cond - (some? type) type - (true? include-libraries) :include-libraries - (true? embed-assets) :merge-libraries - :else :detach-libraries)] + [{:keys [::sto/storage] :as cfg} {:keys [file-id] :as params}] + (let [output (tmp/tempfile*)] (try - (-> cfg - (assoc ::bfc/ids #{file-id}) - (assoc ::bfc/export-type export-type) + (-> (bfc/export-cfg cfg {:ids #{file-id} + :export-type (bfc/resolve-export-type params)}) (bf.v3/export-files! output)) (let [data (sto/content output) @@ -98,22 +90,13 @@ :project-id project-id) cfg - (-> cfg - (assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)) - (assoc ::bfc/project-id project-id) - (assoc ::bfc/profile-id profile-id) - (assoc ::bfc/team-id (:id team)) - (assoc ::bfc/name name) - (assoc ::bfc/import-max-binary-entry-size (cf/get :binfile-import-max-binary-entry-size)) - (assoc ::bfc/import-max-text-entry-size (cf/get :binfile-import-max-text-entry-size)) - (assoc ::bfc/import-max-text-total-size (cf/get :binfile-import-max-text-total-size)) - (assoc ::bfc/import-max-zip-entries (cf/get :binfile-import-max-zip-entries))) + (bfc/import-cfg cfg {:profile-id profile-id + :project-id project-id + :team team + :name name + :input (:path file)}) - input-path (:path file) - owned? (some? upload-id) - - cfg - (assoc cfg ::bfc/input input-path) + owned? (some? upload-id) result (try @@ -126,7 +109,7 @@ :version version}))) (finally (when owned? - (fs/delete input-path))))] + (fs/delete (:path file)))))] (db/update! pool :project {:modified-at (ct/now)} diff --git a/backend/src/app/rpc/commands/jobs.clj b/backend/src/app/rpc/commands/jobs.clj new file mode 100644 index 0000000000..d1aa5b85cb --- /dev/null +++ b/backend/src/app/rpc/commands/jobs.clj @@ -0,0 +1,355 @@ +;; 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 SUBSIDIARY SL + +(ns app.rpc.commands.jobs + "Creation of durable, user-facing jobs. + + One command per job type: the body carries the business params its + job-def declares and nothing else, and the job-def schema is the single + place that says what those params are. Creating a job never runs it: it + freezes what the job will do, and the work belongs to the runner." + (:require + [app.binfile.common :as bfc] + [app.binfile.v3 :as bf.v3] + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.schema :as sm] + [app.common.time :as ct] + [app.config :as cf] + [app.db :as db] + [app.jobs :as jobs] + [app.jobs.storage :as js] + [app.loggers.audit :as-alias audit] + [app.loggers.webhooks :as-alias webhooks] + [app.rpc :as-alias rpc] + [app.rpc.climit :as-alias climit] + [app.rpc.commands.files :as files] + [app.rpc.commands.media :as media] + [app.rpc.commands.projects :as projects] + [app.rpc.doc :as-alias doc] + [app.rpc.quotes :as quotes] + [app.storage :as sto] + [app.tasks.export-binfile :as export-binfile] + [app.tasks.import-binfile :as import-binfile] + [app.util.services :as sv] + [datoteka.fs :as fs])) + +(def ^:private schema:create-export-binfile-job + "The `.penpot` package of the files frozen in the params, produced by + the backend runner; the business params are what the `:export-binfile` + job-def declares." + [:map {:title "create-export-binfile-job" :closed true} + [:params export-binfile/schema:params]]) + +(def ^:private schema:create-import-binfile-job + "The `.penpot` package the caller uploaded, imported into a project by + the backend runner; the business params are what the `:import-binfile` + job-def declares for creation." + [:map {:title "create-import-binfile-job" :closed true} + [:params import-binfile/schema:create-params] + ;; the package arrives as a chunked upload and never as a path the + ;; caller names: see `assemble-upload` + [:upload-id ::sm/uuid]]) + +(def ^:private schema:job-summary + "What the caller gets when a job is created: enough to follow it, and + nothing about the work it will do." + [:map {:title "job-summary"} + [:id ::sm/uuid] + [:status ::sm/text] + [:name ::sm/text] + [:created-at ::ct/inst] + [:expires-at [:maybe ::ct/inst]]]) + +(defn- resolve-job-def + "The job the caller named, refused when it is not a job of `family`: the + registry is the source of truth, and a name that is not one of its jobs + is rejected before anything is read or stored. + + The job-def is returned because the caller needs it right away: it is + what decodes and validates the business params." + [cfg family name] + (let [job-def (get (jobs/get-defs cfg) (keyword name))] + (when-not (= family (::jobs/family job-def)) + (ex/raise :type :validation + :code :not-a-job-of-the-family + :hint "the job name is not a registered job of that family" + :name (d/name name) + :family (d/name family))) + job-def)) + +(defn- get-job-summary + [cfg job-id] + (let [job (jobs/get-job cfg job-id)] + {:id (:id job) + :status (jobs/get-user-status (:status job)) + :name (:name job) + :created-at (:created-at job) + :expires-at (:expires-at job)})) + +(defn- submit-job + "Create the job row with what every user job shares: the queue its + job-def routes the heavy work to (`::jobs/queue-name`, `:binfile` when + the def says nothing), no automatic retry and the expiry of the ledger." + [cfg job-def name params profile-id & {:keys [resource-id]}] + (jobs/submit cfg (cond-> {::jobs/name name + ::jobs/params params + ::jobs/queue (or (::jobs/queue-name job-def) + :binfile) + ::jobs/profile-id profile-id + ::jobs/max-retries 0 + ::jobs/expires-at (ct/plus (ct/now) + (cf/get-jobs-user-ttl))} + (some? resource-id) + (assoc ::jobs/resource-id resource-id)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; EXPORT +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(sv/defmethod ::create-export-binfile-job + "Create a durable job that exports a set of files as a `.penpot` + package. + + Its params are `:file-ids`, the set of files to export, and + `:export-type`, how the libraries of those files are handled: what + the job-def declares, and nothing more. There is no default for the + type, because a caller that does not say it would get a package that + is not the one it asked for. + + The job is created, not run: the caller follows it by its id. The + read permission of every file is checked here so a refusal arrives + synchronously, and the handler checks it again before reading." + {::doc/added "2.20" + ::webhooks/event? true + ::sm/params schema:create-export-binfile-job + ::sm/result schema:job-summary} + [cfg {:keys [::rpc/profile-id] :as envelope}] + (let [job-def (resolve-job-def cfg :export :export-binfile) + ;; the RPC layer decoded the params against the job-def schema + ;; before the command ran + params (jobs/validate-params job-def (:params envelope)) + file-ids (:file-ids params)] + + (when (empty? file-ids) + (ex/raise :type :validation + :code :no-files-to-export + :hint "expected at least one file to export")) + + (doseq [file-id file-ids] + (files/check-read-permissions! cfg profile-id file-id)) + + (quotes/check! cfg {::quotes/id ::quotes/export-jobs-per-profile + ::quotes/profile-id profile-id}) + + (let [summary (get-job-summary cfg (submit-job cfg job-def :export-binfile + params profile-id))] + (with-meta summary + {::audit/props {:job-id (:id summary) + :files (count file-ids) + :export-type (d/name (:export-type params))}})))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; IMPORT +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- assemble-upload + "The package the caller uploaded, assembled into a local file that + belongs to this call. + + The command only takes a chunked upload: a plain file upload would mean + reading a path the caller named, and the server must never read a file + it did not write itself." + [cfg profile-id upload-id] + (db/tx-run! cfg media/assemble-chunks profile-id upload-id)) + +(defn- resolve-version + "The version of the package: what the caller says, or what the header + of the file says." + [path version] + (or version + (case (bfc/parse-file-format path) + :binfile-v3 3 + 1))) + +(sv/defmethod ::create-import-binfile-job + "Create a durable job that imports a `.penpot` package into a project. + + The package arrives as a chunked upload, which the server assembles + itself: the command never reads a path the caller names. The package is + then checked and stored as the resource of the job, so any worker can + consume it by `resource_id`. The job is created, not run, and the caller + follows it by its id." + {::doc/added "2.20" + ::webhooks/event? true + ::sm/params schema:create-import-binfile-job + ::sm/result schema:job-summary + ;; assembling the upload and storing the package happens here, in the + ;; request: the same limit the legacy import command declares + ::climit/id [[:create-import-binfile-job/by-profile ::rpc/profile-id] + [:create-import-binfile-job/global]]} + [cfg {:keys [::rpc/profile-id] :as envelope}] + (let [job-def (resolve-job-def cfg :import :import-binfile) + ;; the RPC layer decoded the params against the create schema + ;; before the command ran + given (:params envelope) + project-id (:project-id given)] + + ;; the edition permission is checked here so a refusal arrives + ;; synchronously and before the upload is assembled; the handler + ;; checks it again before writing + (projects/check-edition-permissions! cfg profile-id project-id) + + (quotes/check! cfg {::quotes/id ::quotes/import-jobs-per-profile + ::quotes/profile-id profile-id}) + + ;; the shape of what the caller sent is checked before the upload + ;; is assembled: the version is known good here (the create schema + ;; rejects one out of range, and the caller may omit it), so the + ;; merged map below only adds what the package says + (jobs/validate-params job-def (assoc given :version (or (:version given) 1))) + + (let [file (assemble-upload cfg profile-id (:upload-id envelope)) + path (:path file)] + + ;; everything that happens once the upload is on disk belongs to + ;; this try: a failure anywhere in it must not leave the temporary + ;; file behind + ;; + ;; the version and the manifest metadata are resolved here, in the + ;; command, and not in the job: only the creation path reads the + ;; package, and the job keeps what the audit will show + (try + (let [version (resolve-version path (:version given)) + manifest (when (= 3 version) + (bf.v3/get-manifest cfg path)) + ;; the manifest may name the tool that produced the package + ;; with either spelling: resolve it once, for the params and + ;; for the audit + referer (or (:referer manifest) (:refer manifest)) + params (cond-> (assoc given :version version) + manifest + (assoc :generated-by (:generated-by manifest) + :referer referer))] + + ;; the params are complete here: the version came from the + ;; caller or from the package, and the manifest filled the audit + (jobs/validate-params job-def params) + + (let [staged (js/put-resource cfg profile-id + {:content (sto/content path) + :filename "package.penpot" + :mtype "application/zip"})] + (try + (let [summary (get-job-summary cfg (submit-job cfg job-def :import-binfile params profile-id + :resource-id (:resource-id staged)))] + (with-meta summary + {::audit/props {:job-id (:id summary) + :generated-by (:generated-by manifest) + :referer referer}})) + + (catch Throwable cause + ;; the job never came to own the package, so it must not be + ;; left for the storage GC to find late + (js/release-resource cfg (:resource-id staged)) + (throw cause))))) + + (finally + ;; the assembled file belongs to this call: the package is + ;; already in storage by the time we get here, or the creation + ;; failed and it is of no use to anyone + (fs/delete path)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; READ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private schema:get-job + [:map {:title "get-job" :closed true} + [:id ::sm/uuid]]) + +(def ^:private schema:job-detail + "The summary of a job plus what only exists once it is over: the result + it produced or, when it failed, its public error." + [:map {:title "job-detail"} + [:id ::sm/uuid] + [:status ::sm/text] + [:name ::sm/text] + [:created-at ::ct/inst] + [:expires-at [:maybe ::ct/inst]] + [:result {:optional true} :any] + [:error {:optional true} :any]]) + +(defn- get-job-detail + "The job the caller owns, as the answer of `get-job`. A job that does + not exist and a job of another profile get the same answer: the caller + cannot tell them apart." + [cfg profile-id job-id] + (let [job (jobs/get-job cfg job-id)] + (when-not (and (some? job) (= profile-id (:profile-id job))) + (ex/raise :type :not-found + :code :job-not-found + :hint "the job does not exist or belongs to another profile" + :job-id job-id)) + (cond-> {:id (:id job) + :status (jobs/get-user-status (:status job)) + :name (:name job) + :created-at (:created-at job) + :expires-at (:expires-at job)} + (some? (:result job)) + (assoc :result (:result job)) + + (some? (:error job)) + (assoc :error (jobs/decode-job-error (:error job)))))) + +(sv/defmethod ::get-job + "Read a job the caller created: its state and, once it is over, what it + produced or why it failed. + + The result of a finished job is the plain JSON the row holds, because a + result has no schema of its own like the params do: a uuid comes back as + the text it is on disk. The progress and the history of the job are not + part of the answer: a client that follows a job takes its progress from + the websocket, and the event log still has no read surface." + {::doc/added "2.20" + ::sm/params schema:get-job + ::sm/result schema:job-detail} + [cfg {:keys [::rpc/profile-id] :as params}] + (get-job-detail cfg profile-id (:id params))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; CANCEL +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private schema:cancel-job + [:map {:title "cancel-job" :closed true} + [:id ::sm/uuid]]) + +(sv/defmethod ::cancel-job + "Cancel a job the caller created, whether it is still waiting or + already running. + + A running job stops at its next beat and rolls its work back; a job + that already finished between the request and the update is answered + with the state it is in, never an error. Like every other command + here, a job of another profile is indistinguishable from one that does + not exist." + {::doc/added "2.20" + ::sm/params schema:cancel-job + ::sm/result schema:job-summary} + [cfg {:keys [::rpc/profile-id] :as params}] + (let [job-id (:id params) + job (jobs/get-job cfg job-id)] + (when-not (and (some? job) (= profile-id (:profile-id job))) + (ex/raise :type :not-found + :code :job-not-found + :hint "the job does not exist or belongs to another profile" + :job-id job-id)) + (jobs/cancel cfg job-id) + ;; the answer names the outcome of this call, not the collapsed + ;; user status: a job the caller just cancelled reads "cancelled" + (assoc (get-job-summary cfg job-id) + :status (:status (jobs/get-job cfg job-id))))) diff --git a/backend/src/app/rpc/management/jobs.clj b/backend/src/app/rpc/management/jobs.clj index ab1561e704..1e71acb101 100644 --- a/backend/src/app/rpc/management/jobs.clj +++ b/backend/src/app/rpc/management/jobs.clj @@ -16,6 +16,7 @@ which mandates a valid shared-key before dispatching to these methods. If the route resolver changes, these methods must be updated accordingly." (:require + [app.common.exceptions :as ex] [app.common.schema :as sm] [app.common.time :as ct] [app.jobs :as jobs] @@ -60,8 +61,8 @@ (def ^:private schema:report-job-progress-params [:map {:title "report-job-progress-params"} ;; The progress schema is the same one the jobs substrate validates: - ;; `current` is mandatory, `total` and `stage` are optional, and no - ;; other key is accepted. + ;; `:stage` is mandatory (a keyword of the worker vocabulary) and + ;; `:counters` is an optional map of `scope -> {:current :total?}`. [:job-id ::sm/uuid] [:progress jobs/schema:progress]]) @@ -78,13 +79,25 @@ ;; A lost race (row already terminal) reports :skip so the worker ;; stops retrying a report that can never land, mirroring claim-job. ;; The forced route propagates database errors: the external worker - ;; must be able to retry a report it was not sure about. - (if (pos? (jobs/heartbeat cfg - :job-id job-id - :progress progress - ::jobs/force? true)) - {:action :run} - {:action :skip})) + ;; must be able to retry a report it was not sure about. The interrupt + ;; the beat raises when the job is no longer active is caught here for + ;; the same reason, and it never reaches the HTTP error mapping: it is + ;; an internal signal, not a failure of the request. + (let [result (ex/try! (jobs/heartbeat cfg + :job-id job-id + :progress progress + ::jobs/force? true))] + (cond + (ex/exception? result) + (if (= :interrupt (:type (ex-data result))) + {:action :skip} + (throw result)) + + (pos? result) + {:action :run} + + :else + {:action :skip}))) ;; ---- RPC METHOD: COMPLETE-JOB diff --git a/backend/src/app/rpc/quotes.clj b/backend/src/app/rpc/quotes.clj index 2cbe336a8d..049728c3a0 100644 --- a/backend/src/app/rpc/quotes.clj +++ b/backend/src/app/rpc/quotes.clj @@ -7,6 +7,7 @@ (ns app.rpc.quotes "Penpot resource usage quotes." (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.schema :as sm] @@ -617,6 +618,61 @@ team-id team-id team-id team-id team-id team-id]) (generic-check!))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; QUOTE: BINFILES JOBS PER PROFILE +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private schema:jobs-per-profile + [:map [::profile-id ::sm/uuid]]) + +(def ^:private valid-jobs-per-profile-quote? + (sm/lazy-validator schema:jobs-per-profile)) + +;; Only the jobs that are still on their way count: a terminal job is not +;; pending work anymore, and the jobs GC removes it. The tenant filters +;; like every other query of the substrate: instances share one database +;; and one profile can have work in more than one of them. +(def ^:private + sql:get-jobs-per-profile + "SELECT count(*) AS total + FROM job + WHERE tenant = ? + AND profile_id = ? + AND status IN ('new', 'scheduled', 'running', 'retry') + AND name = ANY(?)") + +(defn- names-of-family + "The registered names of a family of jobs. The quote counts what the + registry knows, so a new job of the family is covered without touching + this file." + [cfg family] + (->> (jobs/get-defs cfg) + (vals) + (filter #(= family (::jobs/family %))) + (map #(d/name (::jobs/name %))) + (vec))) + +(defn- check-jobs-quote + [cfg family config-key] + (let [{:keys [::db/conn ::profile-id ::target]} cfg] + (assert (valid-jobs-per-profile-quote? cfg) "invalid quote parameters") + (-> cfg + (assoc ::default (cf/get config-key Integer/MAX_VALUE)) + (assoc ::quote-sql [sql:get-quotes-1 target profile-id]) + (assoc ::count-sql [sql:get-jobs-per-profile + (cf/get :tenant) + profile-id + (db/create-array conn "text" (names-of-family cfg family))]) + (generic-check!)))) + +(defmethod check-quote ::export-jobs-per-profile + [cfg] + (check-jobs-quote cfg :export :quotes-export-jobs-per-profile)) + +(defmethod check-quote ::import-jobs-per-profile + [cfg] + (check-jobs-quote cfg :import :quotes-import-jobs-per-profile)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; QUOTE: DEFAULT ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index 6c7a87762b..f73fd3557e 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -248,11 +248,15 @@ backend' (impl/resolve-backend storage backend) bucket (:bucket mdata) + ;; `job-resource` objects belong to one profile, so they are + ;; never shared: a dedup hit would hand the artifact of one + ;; profile to the job of another. dedupable? (and (::deduplicate? params) (:hash mdata) (some? bucket) (not= tempfile-bucket bucket) - (not= upload-session-bucket bucket)) + (not= upload-session-bucket bucket) + (not= job-resource-bucket bucket)) hit (when dedupable? (get-database-object-by-hash pool backend bucket (:hash mdata)))] diff --git a/backend/src/app/storage/schema.clj b/backend/src/app/storage/schema.clj index 17e858b972..50a354d12f 100644 --- a/backend/src/app/storage/schema.clj +++ b/backend/src/app/storage/schema.clj @@ -53,7 +53,8 @@ "organization" #{:organization-id} tempfile-bucket #{} upload-session-bucket #{} - job-resource-bucket #{} + ;; read back only by the profile that owns them + job-resource-bucket #{:profile-id} "file-data" #{:file-id :id} "file-data-fragment" #{} "file-change" #{}}) diff --git a/backend/src/app/tasks/export_binfile.clj b/backend/src/app/tasks/export_binfile.clj new file mode 100644 index 0000000000..eaa1ba0f55 --- /dev/null +++ b/backend/src/app/tasks/export_binfile.clj @@ -0,0 +1,94 @@ +;; 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 SUBSIDIARY SL + +(ns app.tasks.export-binfile + "The `:export-binfile` job: it produces the `.penpot` artifact of the + set of files frozen in its params." + (:require + [app.binfile.jobs :as bfj] + [app.common.schema :as sm] + [app.db :as db] + [app.jobs :as jobs] + [app.jobs.storage :as js] + [app.metrics :as mtx] + [app.rpc.commands.files :as files] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [datoteka.fs :as fs] + [integrant.core :as ig])) + +(def schema:params + "Business params of an export: the files to export, frozen when the job + was created, and how their libraries are handled." + [:map {:title "export-binfile-params" :closed true} + [:file-ids [::sm/set ::sm/uuid]] + [:export-type [::sm/one-of #{:include-libraries :merge-libraries + :detach-libraries :link-later}]]]) + +(def ^:private artifact + "What the user sees of the package an export produces. A single name + describes a set of files, so it does not come from any of them." + {:filename "export.penpot" + :mtype "application/zip"}) + +(def schema:result + "What the job stores when the export succeeds: where the artifact can be + downloaded from and what it is." + [:map {:title "export-binfile-result" :closed true} + [:resource-uri ::sm/text] + [:filename ::sm/text] + [:mtype ::sm/text] + [:size ::sm/int]]) + +(def ^:private check-result + (sm/check-fn schema:result + :hint "invalid result of an export job" + :type :validation + :code :invalid-result)) + +(defn execute-export + "Plain handler, importable and testable without integrant. + + The read permission is checked again here, and not only when the job was + created: it can be revoked while the job waits in its queue. The artifact + is stored as the resource of the job, and the completion envelope carries + its descriptor as the business result and its id for the runner to + associate." + [cfg context params] + (let [profile-id (:profile-id context) + file-ids (:file-ids params)] + + (doseq [file-id file-ids] + (files/check-read-permissions! cfg profile-id file-id)) + + (let [output (tmp/tempfile* :suffix ".penpot")] + (try + (bfj/export-files cfg context {:ids file-ids + :export-type (:export-type params) + :output output}) + (let [resource (js/put-resource cfg profile-id + (assoc artifact :content (sto/content output)))] + {:result (check-result (dissoc resource :resource-id)) + :resource-id (:resource-id resource)}) + (finally + (fs/delete output)))))) + +(defmethod ig/assert-key ::job-def + [_ params] + (assert (db/pool? (::db/pool params)) "expected a valid database pool") + (assert (sto/valid-storage? (::sto/storage params)) "expected valid storage to be provided") + (assert (mtx/metrics? (::mtx/metrics params)) "expected valid metrics")) + +(defmethod ig/init-key ::job-def + [_ cfg] + {::jobs/name :export-binfile + ::jobs/family :export + ::jobs/resource-role :output + ::jobs/schema schema:params + ::jobs/handler (fn [context params] + (execute-export cfg context params)) + ::jobs/decoder (sm/decoder schema:params sm/json-transformer) + ::jobs/validator (sm/validator schema:params)}) diff --git a/backend/src/app/tasks/import_binfile.clj b/backend/src/app/tasks/import_binfile.clj new file mode 100644 index 0000000000..5dc286db08 --- /dev/null +++ b/backend/src/app/tasks/import_binfile.clj @@ -0,0 +1,126 @@ +;; 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 SUBSIDIARY SL + +(ns app.tasks.import-binfile + "The `:import-binfile` job: it imports the package the job owns as a + resource into the project frozen in its params." + (:require + [app.binfile.jobs :as bfj] + [app.common.exceptions :as ex] + [app.common.schema :as sm] + [app.db :as db] + [app.jobs :as jobs] + [app.jobs.storage :as js] + [app.metrics :as mtx] + [app.rpc.commands.projects :as projects] + [app.rpc.commands.teams :as teams] + [app.storage :as sto] + [datoteka.fs :as fs] + [integrant.core :as ig])) + +(def schema:params + "Business params of an import: where the package goes, its name, the + format version and the manifest metadata kept for audit." + [:map {:title "import-binfile-params" :closed true} + [:project-id ::sm/uuid] + [:name [:or [:string {:max 250}] + [:map-of ::sm/uuid [:string {:max 250}]]]] + [:version [:enum 1 3]] + [:generated-by {:optional true} [:maybe ::sm/text]] + [:referer {:optional true} [:maybe ::sm/text]]]) + +(def schema:create-params + "The business params of an import as the creation command receives + them: where the package goes and its name. The version is optional + because the command reads it from the package header when the caller + does not say it, and the manifest metadata kept for audit is filled by + the command, never sent by the caller." + [:map {:title "import-binfile-create-params" :closed true} + [:project-id ::sm/uuid] + [:name [:or [:string {:max 250}] + [:map-of ::sm/uuid [:string {:max 250}]]]] + [:version {:optional true} [:enum 1 3]]]) + +(def schema:result + "What the job stores when the import succeeds: the files it created and + how the libraries of the package were resolved." + [:map {:title "import-binfile-result" :closed true} + [:file-ids [:vector ::sm/uuid]] + [:resolution {:optional true} :map] + [:name [:or [:string {:max 250}] + [:map-of ::sm/uuid [:string {:max 250}]]]] + [:version [:enum 1 3]]]) + +(def ^:private check-result + (sm/check-fn schema:result + :hint "invalid result of an import job" + :type :validation + :code :invalid-result)) + +(defn execute-import + "Plain handler, importable and testable without integrant. + + The edition permission is checked again here, and not only when the job + was created: it can be revoked while the job waits in its queue. The + core runs inside a transaction, so a cancellation detected between units + rolls the whole import back. + + The package is the input of the job: it is released and its temporary + copy deleted once the job is terminal, whatever the outcome. A crash is + covered by the storage GC." + [cfg context params] + (let [profile-id (:profile-id context) + project-id (:project-id params) + input (js/load-input cfg context)] + + (try + (projects/check-edition-permissions! cfg profile-id project-id) + + (let [team (or (teams/get-team cfg + :profile-id profile-id + :project-id project-id) + (ex/raise :type :not-found + :code :project-not-found + :hint "the destination project is not available" + :profile-id profile-id + :project-id project-id)) + result (db/tx-run! cfg + (fn [tx-cfg] + ;; the core joins this transaction: it + ;; is the one that decides whether the + ;; whole import is kept or rolled back + (bfj/import-files tx-cfg context + {:profile-id profile-id + :project-id project-id + :team team + :name (:name params) + :input input + :version (:version params)})))] + + (check-result (assoc result + :name (:name params) + :version (:version params)))) + + (finally + (fs/delete input) + (js/release-input cfg context))))) + +(defmethod ig/assert-key ::job-def + [_ params] + (assert (db/pool? (::db/pool params)) "expected a valid database pool") + (assert (sto/valid-storage? (::sto/storage params)) "expected valid storage to be provided") + (assert (mtx/metrics? (::mtx/metrics params)) "expected valid metrics")) + +(defmethod ig/init-key ::job-def + [_ cfg] + {::jobs/name :import-binfile + ::jobs/family :import + ::jobs/resource-role :input + ::jobs/schema schema:params + ::jobs/handler (fn [context params] + (execute-import cfg context params)) + ::jobs/decoder (sm/decoder schema:params sm/json-transformer) + ::jobs/validator (sm/validator schema:params)}) diff --git a/backend/src/app/util/events.clj b/backend/src/app/util/events.clj index ffbd96a47c..1c82380943 100644 --- a/backend/src/app/util/events.clj +++ b/backend/src/app/util/events.clj @@ -5,9 +5,15 @@ ;; Copyright (c) KALEIDOS SUBSIDIARY SL (ns app.util.events - "A generic asynchronous events notifications subsystem; used mainly - for mark event points in functions and be able to attach listeners - to them. Mainly used in http.sse for progress reporting." + "Event points of a run: the core marks what it does with `tap`, and + whoever runs it decides where those marks go. + + The sink bound for the run decides how each event travels: a function + is called inline, on the same thread, while a channel receives the + event for a listener of its own. The jobs adapter binds a function + that turns taps into heartbeats, so an `:interrupt` it raises aborts + the run right there; the SSE stream binds a channel instead, and its + listener writes what arrives to the response." (:refer-clojure :exclude [run!]) (:require [app.common.exceptions :as ex] @@ -15,19 +21,29 @@ [promesa.exec :as px] [promesa.exec.csp :as sp])) -(def ^:dynamic *channel* nil) +(def ^:dynamic *sink* nil) (defn tap - ([type data] - (when-let [channel *channel*] - (sp/put! channel [type data]) - nil)) - ([channel type data] - (when channel - (sp/put! channel [type data]) - nil))) + "Mark an event point of the run in course with `[type data]`. + + With no sink bound nothing happens, so untracked runs pay a single + nil check per event point. A function sink is called inline and any + exception it raises propagates to the producer: that is how an + `:interrupt` from a heartbeat stops the run at its next event point. + A channel sink receives the event for its listener instead." + [type data] + (when-let [sink *sink*] + (if (fn? sink) + (sink [type data]) + (sp/put! sink [type data])) + nil)) (defn spawn-listener + "Consume the events of `channel` on a thread of its own, until it + closes. + + `on-close` runs once the listener stops. A failure of `on-event` is + logged and stops the listener." [channel on-event on-close] (assert (sp/chan? channel) "expected active events channel") @@ -39,22 +55,14 @@ (let [result (ex/try! (on-event event))] (if (ex/exception? result) (do - (l/wrn :hint "unexpected exception" :cause result) + (l/err :hint "unexpected exception on calling-on-event of spawn-listener" + :cause result) (sp/close! channel)) (recur))))) (finally - (on-close))))) - -(defn run-with! - "A high-level facility for to run a function in context of event - emiter." - [f on-event] - - (binding [*channel* (sp/chan :buf 32)] - (let [listener (spawn-listener *channel* on-event (constantly nil))] - (try - (f) - (finally - (sp/close! *channel*) - (px/await! listener)))))) + (try + (on-close) + (catch Exception cause + (l/err :hint "unexpected exception on calling on-close of spawn-listener" + :cause cause))))))) diff --git a/backend/src/app/worker/runner.clj b/backend/src/app/worker/runner.clj index f5506f0450..2202722c0d 100644 --- a/backend/src/app/worker/runner.clj +++ b/backend/src/app/worker/runner.clj @@ -163,9 +163,22 @@ (throw cause)) (catch Throwable cause (let [edata (ex-data cause)] - (if (and (< (:retry-num job) - (:max-retries job)) - (= ::wrk/retry (:type edata))) + (cond + ;; the interrupt is the signal that the job is no longer active: + ;; a normal procedure, so it is not reported as an error and it + ;; never fails or retries a row that is already terminal + (= :interrupt (:type edata)) + (binding [l/*context* (-> (cf/logging-context) + (assoc :params job))] + (l/inf :hint "interrupted" + :tenant tenant + :job-id (str (:job-id edata)) + :status (:status edata)) + {:status "interrupted"}) + + (and (< (:retry-num job) + (:max-retries job)) + (= ::wrk/retry (:type edata))) (cond-> {:status "retry" :error cause} (ct/duration? (:delay edata)) (assoc :delay-ms (inst-ms (:delay edata))) @@ -174,6 +187,8 @@ (= ::wrk/noop (:strategy edata)) (assoc :inc-by 0)) + + :else (do (l/err :hint "unhandled exception on job" ::l/context (assoc (cf/logging-context) :params job) @@ -288,9 +303,12 @@ (process-result [{:keys [status] :as result}] (ex/try! (case status - "retry" (handle-job-retry result) - "failed" (handle-job-failure result) - "completed" (handle-job-completion result) + "retry" (handle-job-retry result) + "failed" (handle-job-failure result) + "completed" (handle-job-completion result) + ;; the job is no longer active: there is nothing to write, + ;; the row is already terminal and whoever stopped it did + "interrupted" nil (throw (IllegalArgumentException. (str "invalid status received: '" status "'")))))) diff --git a/backend/test/backend_tests/binfile_jobs_test.clj b/backend/test/backend_tests/binfile_jobs_test.clj new file mode 100644 index 0000000000..7b4619c96c --- /dev/null +++ b/backend/test/backend_tests/binfile_jobs_test.clj @@ -0,0 +1,403 @@ +;; 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 SUBSIDIARY SL + +(ns backend-tests.binfile-jobs-test + "The binfile work of a job: the shared adapter that runs it with its + progress reported as job events, and the two job-defs that expose it." + (:require + [app.binfile.common :as bfc] + [app.binfile.jobs :as bfj] + [app.binfile.v3 :as v3] + [app.common.time :as ct] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.jobs :as jobs] + [app.jobs.storage :as js] + [app.rpc.commands.teams :as teams] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [app.util.events :as events] + [backend-tests.helpers :as th] + [clojure.java.io :as jio] + [clojure.test :as t] + [datoteka.fs :as fs] + [datoteka.io :as io])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) + +(def ^:private fixture + "A real penpot export, the same fixture the binfile tests read." + "backend_tests/test_files/svg-attrs-camel-case.penpot") + +(defn- fixture-path + [] + (-> fixture io/resource jio/file)) + +(defn- import-fixture! + "A file in the default project of the profile, imported from the + fixture through the core." + [profile] + (-> th/*system* + (assoc ::bfc/project-id (:default-project-id profile)) + (assoc ::bfc/profile-id (:id profile)) + (assoc ::bfc/team-id (:default-team-id profile)) + (assoc ::bfc/input (fixture-path)) + (v3/import-files!))) + +(defn- make-job! + "A job row as the substrate writes it: running, owned by a profile and + holding a resource when the test needs one." + [profile-id resource-id] + (let [id (uuid/next)] + (th/db-insert! :job {:id id + :name "export-binfile" + :tenant (cf/get :tenant) + :queue "binfile" + :params (db/json {}) + :priority 100 + :max-retries 0 + :retry-num 0 + :status "running" + :profile-id profile-id + :resource-id resource-id + :scheduled-at (ct/now) + :created-at (ct/now) + :modified-at (ct/now)}) + id)) + +(defn- job-cfg + "The system cfg as the runner hands it to a handler. The job id is NOT + bound here: the adapter takes it from the context, because the progress + bridge runs on a thread of its own." + [] + th/*system*) + +(defn- job-context + [job-id] + (jobs/make-context (jobs/get-job th/*system* job-id))) + +(defn- progress-events + "The progress events of a job, oldest first, with decoded payloads." + [job-id] + (->> (th/db-exec! ["SELECT payload FROM job_event + WHERE job_id = ? AND kind = 'progress' + ORDER BY created_at ASC, id ASC" + job-id]) + (mapv #(jobs/decode-progress (:payload %))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; ADAPTER +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest export-through-the-adapter-produces-the-artifact + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (make-job! (:id profile) nil) + output (tmp/tempfile* :suffix ".penpot")] + + (bfj/export-files (job-cfg) (job-context job-id) + {:ids #{file-id} + :export-type :detach-libraries + :output output}) + + (t/testing "the artifact is a penpot file with its manifest" + (t/is (fs/exists? output)) + (t/is (= [file-id] (mapv :id (:files (v3/get-manifest th/*system* output)))))) + + (t/testing "the progress of the core became milestones of the job" + (let [events (progress-events job-id)] + (t/is (pos? (count events))) + (t/is (every? keyword? (map :stage events))) + (t/testing "the first one is the file in course, with its position" + (let [event (first events)] + (t/is (= :files (:stage event))) + (t/is (= {:current 1 :total 1} (get-in event [:counters :files]))))) + (t/testing "and nothing else: the event is the stage and the counters" + (t/is (every? #(<= (count (keys %)) 2) events))))))) + +(t/deftest import-through-the-adapter-imports-the-package + (let [profile (th/create-profile* 1) + team (teams/get-team th/*system* + :profile-id (:id profile) + :project-id (:default-project-id profile)) + staged (js/put-resource th/*system* (:id profile) + {:content (sto/content (fixture-path)) + :filename "package.penpot" + :mtype "application/zip"}) + job-id (make-job! (:id profile) (:resource-id staged)) + context (job-context job-id) + result (bfj/import-files (job-cfg) context + {:profile-id (:id profile) + :project-id (:default-project-id profile) + :team team + :name "imported" + :input (js/load-input th/*system* context) + :version 3})] + + (t/testing "the package is imported in the destination project" + (t/is (= 1 (count (:file-ids result))))) + + (t/testing "the job reported its progress" + (t/is (pos? (count (progress-events job-id))))))) + +(t/deftest the-adapter-composes-a-milestone-per-tap + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (make-job! (:id profile) nil) + reports (atom [])] + + ;; the substrate throttles the durable events, so the whole stream of + ;; milestones is only visible with the writes intercepted + (with-redefs [jobs/heartbeat (fn [_cfg & {:as options}] + (swap! reports conj (:progress options)) + 1)] + (bfj/export-files (job-cfg) (job-context job-id) + {:ids #{file-id} + :export-type :detach-libraries + :output (tmp/tempfile* :suffix ".penpot")})) + + (t/testing "the file opens the run with its position" + (let [event (first @reports)] + (t/is (= :files (:stage event))) + (t/is (= {:current 1 :total 1} (get-in event [:counters :files]))))) + + (t/testing "the stages inside the file carry it as context, with their own counter" + (let [event (first (filter #(= :pages (:stage %)) @reports))] + (t/is (= {:current 1 :total 1} (get-in event [:counters :files]))) + (t/is (pos? (get-in event [:counters :pages :current]))) + (t/is (pos? (get-in event [:counters :pages :total]))))) + + (t/testing "a stage outside a file carries no file context" + (let [events (filter #(= :storage-objects (:stage %)) @reports)] + (t/is (pos? (count events))) + (t/is (every? #(nil? (get-in % [:counters :files])) events)))) + + (t/testing "and every milestone is a stage and its counters, and nothing else" + (t/is (every? #(<= (count (keys %)) 2) @reports))))) + +(t/deftest a-tap-carries-the-outer-counters-in-course + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (make-job! (:id profile) nil) + taps (atom [])] + + ;; the taps themselves, before the adapter turns them into events + (with-redefs [events/tap (fn [_type data] (swap! taps conj data))] + (bfj/export-files (job-cfg) (job-context job-id) + {:ids #{file-id} + :export-type :detach-libraries + :output (tmp/tempfile* :suffix ".penpot")})) + + (t/testing "the tap that opens the file carries the outer counters" + (let [tap (first (filter #(= :file (:section %)) @taps))] + (t/is (= {:files {:current 1 :total 1}} (:outer-counters tap))))) + + (t/testing "every tap inside the file carries them as context" + (let [inner (filter #(some? (:file-id %)) @taps)] + (t/is (pos? (count inner))) + (t/is (every? #(= {:files {:current 1 :total 1}} (:outer-counters %)) inner)))) + + (t/testing "a tap outside a file carries no outer counters" + (let [outside (filter #(= :storage-object (:section %)) @taps)] + (t/is (pos? (count outside))) + (t/is (every? #(nil? (:outer-counters %)) outside)))))) + +(t/deftest a-milestone-merges-outer-counters-with-its-own + (let [milestone @#'bfj/milestone] + (t/testing "outer scopes survive next to the counter of the stage" + (t/is (= {:stage :pages + :counters {:teams {:current 1 :total 1} + :projects {:current 2 :total 2} + :files {:current 1 :total 4} + :pages {:current 2 :total 5}}} + (milestone {:section :page + :current 2 :total 5 + :outer-counters {:teams {:current 1 :total 1} + :projects {:current 2 :total 2} + :files {:current 1 :total 4}}})))) + (t/testing "a file tap needs no own counter, the outer one is enough" + (t/is (= {:stage :files + :counters {:files {:current 1 :total 2}}} + (milestone {:section :file + :outer-counters {:files {:current 1 :total 2}}})))))) + +(t/deftest entering-a-file-keeps-the-counters-of-the-outer-scopes + (let [cfg {::v3/outer-counters {:teams {:current 1 :total 1}}} + cfg' (@#'v3/with-outer-counter cfg :files {:current 2 :total 4})] + (t/is (= {:teams {:current 1 :total 1} + :files {:current 2 :total 4}} + (::v3/outer-counters cfg'))))) + +(t/deftest the-import-adapter-composes-the-same-milestones + (let [profile (th/create-profile* 1) + team (teams/get-team th/*system* + :profile-id (:id profile) + :project-id (:default-project-id profile)) + staged (js/put-resource th/*system* (:id profile) + {:content (sto/content (fixture-path)) + :filename "package.penpot" + :mtype "application/zip"}) + job-id (make-job! (:id profile) (:resource-id staged)) + context (job-context job-id) + reports (atom [])] + + (with-redefs [jobs/heartbeat (fn [_cfg & {:as options}] + (swap! reports conj (:progress options)) + 1)] + (bfj/import-files (job-cfg) context + {:profile-id (:id profile) + :project-id (:default-project-id profile) + :team team + :name "imported" + :input (js/load-input th/*system* context) + :version 3})) + + (t/testing "the run opens with the manifest, which has no units to count" + (let [event (first @reports)] + (t/is (= :manifest (:stage event))) + (t/is (nil? (:counters event))))) + + (t/testing "the file carries its position" + (let [event (first (filter #(= :files (:stage %)) @reports))] + (t/is (= {:current 1 :total 1} (get-in event [:counters :files]))))) + + (t/testing "a stage inside the file carries it as context, with its own counter" + (let [event (first (filter #(= :pages (:stage %)) @reports))] + (t/is (= {:current 1 :total 1} (get-in event [:counters :files]))) + (t/is (pos? (get-in event [:counters :pages :current]))) + (t/is (pos? (get-in event [:counters :pages :total]))))) + + (t/testing "and every milestone is a stage and its counters, and nothing else" + (t/is (every? #(<= (count (keys %)) 2) @reports))))) + +(t/deftest a-job-that-is-no-longer-active-stops-the-work + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (make-job! (:id profile) nil) + context (job-context job-id)] + + (t/testing "a running job is active" + (t/is (nil? (bfj/check-active th/*system* context)))) + + (t/testing "a cancelled job is not" + (th/db-update! :job {:status "cancelled"} {:id job-id}) + (let [cause (try (bfj/check-active th/*system* context) nil + (catch Throwable raised raised))] + (t/is (= :interrupt (th/ex-type cause))) + (t/is (= "cancelled" (:status (ex-data cause)))))) + + (t/testing "so the export stops at its next progress point" + (let [output (tmp/tempfile* :suffix ".penpot") + cause (try + (bfj/export-files (job-cfg) context + {:ids #{file-id} + :export-type :detach-libraries + :output output}) + nil + (catch Throwable raised raised))] + (t/is (= :interrupt (th/ex-type cause))) + (t/is (zero? (count (progress-events job-id)))))) + + (t/testing "a job that already completed is not active either" + (th/db-update! :job {:status "completed"} {:id job-id}) + (let [cause (try (bfj/check-active th/*system* context) nil + (catch Throwable raised raised))] + (t/is (= :interrupt (th/ex-type cause))))) + + (t/testing "and neither is a job row that is gone" + (th/db-force-delete :job {:id job-id}) + (let [cause (try (bfj/check-active th/*system* context) nil + (catch Throwable raised raised))] + (t/is (= :interrupt (th/ex-type cause))) + (t/is (= "gone" (:status (ex-data cause)))))))) + +(t/deftest an-interrupted-beat-stops-the-core-at-its-tap + (let [steps (atom []) + context {:id (uuid/next)} + cause (ex-info "the job is no longer active" + {:type :interrupt :code :job-interrupted})] + (with-redefs [jobs/heartbeat (fn [& _] (throw cause))] + (let [raised (try (@#'bfj/with-progress th/*system* context + (fn [] + (swap! steps conj :before) + ;; the sink calls the beat + ;; inline, so its interrupt + ;; aborts the run here + (events/tap :progress {:section :file}) + (swap! steps conj :after))) + nil + (catch Throwable stopped stopped))] + (t/is (= :interrupt (:type (ex-data raised)))) + (t/is (= :job-interrupted (:code (ex-data raised)))) + (t/is (= [:before] @steps) + "the core stopped at its next tap, the step after never ran"))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; JOB-DEFS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest the-binfile-jobs-follow-the-substrate-contract + (let [defs (::jobs/defs th/*system*)] + + (t/testing "the export job produces the resource of a user job" + (let [def (jobs/get-job-def defs :export-binfile)] + (t/is (= :export (::jobs/family def))) + (t/is (= :output (::jobs/resource-role def))) + (t/is (fn? (::jobs/handler def))))) + + (t/testing "the import job consumes the resource of the job" + (let [def (jobs/get-job-def defs :import-binfile)] + (t/is (= :import (::jobs/family def))) + (t/is (= :input (::jobs/resource-role def))) + (t/is (fn? (::jobs/handler def))))) + + (t/testing "the params are a closed contract" + (let [def (jobs/get-job-def defs :export-binfile) + file-id (uuid/next)] + (t/is (true? ((::jobs/validator def) + {:file-ids #{file-id} :export-type :detach-libraries}))) + (t/testing "a job id never travels in params" + (t/is (false? ((::jobs/validator def) + {:file-ids #{file-id} :export-type :detach-libraries + :job-id (uuid/next)})))))))) + +(t/deftest the-export-handler-runs-through-the-registry + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (make-job! (:id profile) nil) + result (jobs/invoke (-> th/*system* + (assoc ::jobs/name :export-binfile) + (assoc ::jobs/params {:file-ids #{file-id} + :export-type :detach-libraries}) + (assoc ::jobs/context (job-context job-id))))] + + (t/testing "the handler answers with the completion envelope of the job" + (t/is (uuid? (:resource-id result))) + (t/is (some? (get-in result [:result :resource-uri])))) + + (t/testing "the handler reported progress without knowing the job id" + (t/is (pos? (count (progress-events job-id))))))) + +(t/deftest the-import-handler-runs-through-the-registry + (let [profile (th/create-profile* 1) + staged (js/put-resource th/*system* (:id profile) + {:content (sto/content (fixture-path)) + :filename "package.penpot" + :mtype "application/zip"}) + job-id (make-job! (:id profile) (:resource-id staged)) + result (jobs/invoke (-> th/*system* + (assoc ::jobs/name :import-binfile) + (assoc ::jobs/params {:project-id (:default-project-id profile) + :name "imported" + :version 3}) + (assoc ::jobs/context (job-context job-id))))] + + (t/testing "the package of the job was imported" + (t/is (pos? (count (:file-ids result))))) + (t/testing "the handler reported progress without knowing the job id" + (t/is (pos? (count (progress-events job-id))))))) diff --git a/backend/test/backend_tests/config_test.clj b/backend/test/backend_tests/config_test.clj index 863be00a5e..af06598962 100644 --- a/backend/test/backend_tests/config_test.clj +++ b/backend/test/backend_tests/config_test.clj @@ -6,6 +6,7 @@ (ns backend-tests.config-test (:require + [app.common.time :as ct] [app.config :as cf] [clojure.test :as t])) @@ -51,11 +52,26 @@ (t/testing "schema decodes all runner parallelism keys (env strings to int)" (let [decoded (cf/decode-config {:worker-default-parallelism "4" :worker-webhook-parallelism "2" - :worker-cron-parallelism "3"})] + :worker-cron-parallelism "3" + :worker-binfile-parallelism "5"})] (t/is (= 4 (:worker-default-parallelism decoded))) (t/is (= 2 (:worker-webhook-parallelism decoded))) - (t/is (= 3 (:worker-cron-parallelism decoded))))) + (t/is (= 3 (:worker-cron-parallelism decoded))) + (t/is (= 5 (:worker-binfile-parallelism decoded))))) (t/testing "lookups resolve configured values (unnamespaced keys, as main uses)" (with-redefs [cf/config (assoc cf/config :worker-default-parallelism 4)] (t/is (= 4 (cf/get :worker-default-parallelism 1)))))) + +(t/deftest jobs-user-ttl + (t/testing "the env value is decoded as a duration" + (let [decoded (cf/decode-config {:jobs-user-ttl "72h"})] + (t/is (= (ct/duration {:days 3}) (:jobs-user-ttl decoded))))) + + (t/testing "the getter resolves the configured value" + (with-redefs [cf/config (assoc cf/config :jobs-user-ttl (ct/duration {:days 1}))] + (t/is (= (ct/duration {:days 1}) (cf/get-jobs-user-ttl))))) + + (t/testing "and falls back to a week" + (with-redefs [cf/config (dissoc cf/config :jobs-user-ttl)] + (t/is (= (ct/duration {:days 7}) (cf/get-jobs-user-ttl)))))) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index d89250eee3..d39e2a26a6 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -102,6 +102,22 @@ (fs/create-dir "/tmp/penpot") (next))))) +(defn- remove-job-runners + "Drops every `[ :app.worker/runner]` from the system. + + The tests drive the runner loop by hand (`wdisp/run-batch` plus + `run-worker-loop` over a system they build themselves), so a live runner + of any queue races them: it takes the job the test just dispatched and + completes it behind the test's back. Matching the key by shape, instead + of listing the known queues by hand, keeps a new queue from silently + bringing that race back (which is what happened to the `binfile` runner + this key list was missing)." + [system] + (apply dissoc system + (filter #(and (vector? %) + (= :app.worker/runner (second %))) + (keys system)))) + (defn init-system [next] (let [templates [{:id "test" @@ -132,10 +148,8 @@ :app.loggers.mattermost/reporter :app.loggers.database/reporter :app.worker/cron - :app.worker/dispatcher - [:app.main/default :app.worker/runner] - [:app.main/webhook :app.worker/runner] - [:app.main/cron :app.worker/runner])) + :app.worker/dispatcher) + (remove-job-runners)) _ (ig/load-namespaces system) system (-> (ig/expand system) (ig/init))] (try diff --git a/backend/test/backend_tests/http_assets_test.clj b/backend/test/backend_tests/http_assets_test.clj index a5e60e9965..1f6c473a51 100644 --- a/backend/test/backend_tests/http_assets_test.clj +++ b/backend/test/backend_tests/http_assets_test.clj @@ -887,6 +887,48 @@ response (assets/objects-handler cfg request)] (t/is (= 204 (::yres/status response))))) +;; ---------------------------------------------------------------- +;; Tests: objects-handler — job-resource bucket ownership +;; ---------------------------------------------------------------- + +(t/deftest objects-handler-job-resource-owner-can-access + (let [storage (-> (:app.storage/storage th/*system*) + (configure-storage-backend)) + cfg (make-handler-cfg storage) + owner (th/create-profile* 1) + object (create-storage-object! storage "job-resource" "artifact" {:profile-id (:id owner)}) + request {:path-params {:id (str (:id object))} + ::session/profile-id (:id owner)} + response (assets/objects-handler cfg request)] + (t/is (= 204 (::yres/status response))))) + +(t/deftest objects-handler-job-resource-non-owner-gets-404 + ;; The artifact of a job belongs to the profile that owns the job. + (let [storage (-> (:app.storage/storage th/*system*) + (configure-storage-backend)) + cfg (make-handler-cfg storage) + owner (th/create-profile* 1) + stranger (th/create-profile* 2) + object (create-storage-object! storage "job-resource" "artifact" {:profile-id (:id owner)}) + request {:path-params {:id (str (:id object))} + ::session/profile-id (:id stranger)} + response (assets/objects-handler cfg request)] + (t/is (= 404 (::yres/status response))))) + +(t/deftest objects-handler-job-resource-released-object-gets-404 + ;; Once the job is gone and the GC has reclaimed the artifact, the route + ;; has nothing to serve. + (let [storage (-> (:app.storage/storage th/*system*) + (configure-storage-backend)) + cfg (make-handler-cfg storage) + owner (th/create-profile* 1) + object (create-storage-object! storage "job-resource" "artifact" {:profile-id (:id owner)}) + request {:path-params {:id (str (:id object))} + ::session/profile-id (:id owner)}] + (t/is (= 204 (::yres/status (assets/objects-handler cfg request)))) + (sto/del-object! storage (:id object)) + (t/is (= 404 (::yres/status (assets/objects-handler cfg request)))))) + ;; ---------------------------------------------------------------- ;; Tests: asset request metrics ;; ---------------------------------------------------------------- diff --git a/backend/test/backend_tests/jobs_metrics_test.clj b/backend/test/backend_tests/jobs_metrics_test.clj index af43e856f3..45f4b59725 100644 --- a/backend/test/backend_tests/jobs_metrics_test.clj +++ b/backend/test/backend_tests/jobs_metrics_test.clj @@ -298,10 +298,29 @@ (fn [{:keys [::db/conn]}] (jobs/heartbeat (assoc cfg ::db/conn conn) :job-id job-id - :progress {:current 1} + :progress {:stage :work} ::jobs/force? true))) (t/testing "the report is stored and counted, the caller rollback drops neither" (t/is (= 1 (:cnt (th/db-exec-one! ["SELECT count(*) AS cnt FROM job_event WHERE job_id = ? AND kind = 'progress'" job-id])))) (t/is (= (inc before) (counter-value metrics :jobs-events-total []))))))) + +(t/deftest the-binfile-queue-is-labelled-like-any-other + (let [metrics (make-metrics) + defs {:export-binfile {::jobs/name :export-binfile + ::jobs/schema [:map] + ::jobs/handler (fn [_context params] params) + ::jobs/decoder identity + ::jobs/validator (constantly true)}} + cfg {::jobs/defs defs + ::db/pool th/*pool* + ::mtx/metrics metrics}] + + (jobs/submit cfg {::jobs/name :export-binfile + ::jobs/params {} + ::jobs/queue :binfile}) + + (t/testing "the queue of the job is its label, never other" + (t/is (= 1.0 (counter-value metrics :jobs-submitted ["export-binfile" "binfile"]))) + (t/is (= 0.0 (counter-value metrics :jobs-submitted ["export-binfile" "other"])))))) diff --git a/backend/test/backend_tests/jobs_storage_test.clj b/backend/test/backend_tests/jobs_storage_test.clj new file mode 100644 index 0000000000..3da1f29525 --- /dev/null +++ b/backend/test/backend_tests/jobs_storage_test.clj @@ -0,0 +1,205 @@ +;; 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 SUBSIDIARY SL + +(ns backend-tests.jobs-storage-test + "The storage helpers of the unified jobs substrate: the objects a job + owns through `job.resource_id`." + (:require + [app.common.time :as ct] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.jobs.storage :as js] + [app.storage :as sto] + [backend-tests.helpers :as th] + [clojure.test :as t] + [cuerdas.core :as str] + [datoteka.fs :as fs])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each (th/serial th/database-reset th/clean-storage)) + +(defn- get-storage + [] + (assoc (:app.storage/storage th/*system*) ::sto/backend :fs)) + +(defn- make-cfg + [] + {::db/pool th/*pool* + ::sto/storage (get-storage)}) + +(defn- get-object-row + "The `storage_object` row, deleted rows included." + [id] + (th/db-get :storage-object {:id id} {::db/remove-deleted false})) + +(defn- put-artifact + [cfg profile-id content] + (js/put-resource cfg profile-id {:content (sto/content content) + :filename "artifact.penpot" + :mtype "application/zip"})) + +(t/deftest put-resource-stores-the-artifact-for-its-owner + (let [cfg (make-cfg) + profile-id (:id (th/create-profile* 1)) + result (put-artifact cfg profile-id "penpot-bytes") + object (sto/get-object (get-storage) (:resource-id result)) + row (get-object-row (:resource-id result))] + + (t/testing "the object lives in the job-resource bucket" + (t/is (= sto/job-resource-bucket (:bucket (meta object))))) + + (t/testing "the owner and the content type travel in the metadata" + (t/is (= profile-id (:profile-id (meta object)))) + (t/is (= "application/zip" (:content-type (meta object))))) + + (t/testing "the object is readable: retention comes from the job row" + (t/is (nil? (:deleted-at row)))) + + (t/testing "it is touched right away so a crash leaves it reclaimable" + (t/is (some? (:touched-at row)))) + + (t/testing "the descriptor identifies the artifact" + (t/is (uuid? (:resource-id result))) + (t/is (= 12 (:size result))) + (t/is (= "artifact.penpot" (:filename result))) + (t/is (= "application/zip" (:mtype result))) + (t/is (str/includes? (str (:resource-uri result)) + (str (:resource-id result))))))) + +(t/deftest put-resource-requires-an-owner + (let [cfg (make-cfg)] + (t/testing "an artifact without an owner is a caller bug" + (t/is (thrown? Throwable + (js/put-resource cfg nil {:content (sto/content "x") + :filename "x.penpot" + :mtype "application/zip"})))))) + +(t/deftest load-input-gives-the-handler-a-local-path + (let [cfg (make-cfg) + profile-id (:id (th/create-profile* 1)) + staged (put-artifact cfg profile-id "package-bytes") + context {:id (uuid/next) + :resource-id (:resource-id staged) + :profile-id profile-id} + path (js/load-input cfg context)] + + (t/is (some? path)) + (t/is (= "package-bytes" (slurp (fs/file path)))))) + +(t/deftest load-input-refuses-an-input-of-another-profile + (let [cfg (make-cfg) + owner (:id (th/create-profile* 1)) + stranger (:id (th/create-profile* 2)) + staged (put-artifact cfg owner "package-bytes") + context {:id (uuid/next) + :resource-id (:resource-id staged) + :profile-id stranger}] + + (t/testing "a handler never reads a package that is not its job's" + (t/is (thrown? Exception (js/load-input cfg context)))))) + +(t/deftest load-input-refuses-a-missing-or-released-input + (let [cfg (make-cfg) + profile-id (:id (th/create-profile* 1)) + staged (put-artifact cfg profile-id "package-bytes") + context {:id (uuid/next) + :resource-id (:resource-id staged) + :profile-id profile-id}] + + (t/testing "a job without a resource reference has nothing to load" + (t/is (thrown? Exception (js/load-input cfg (dissoc context :resource-id))))) + + (t/testing "an input that was already released cannot be read again" + (t/is (true? (js/release-input cfg context))) + (t/is (thrown? Exception (js/load-input cfg context)))))) + +(t/deftest release-input-is-idempotent-and-never-throws + (let [cfg (make-cfg) + profile-id (:id (th/create-profile* 1)) + staged (put-artifact cfg profile-id "package-bytes") + context {:id (uuid/next) + :resource-id (:resource-id staged) + :profile-id profile-id}] + + (t/is (true? (js/release-input cfg context))) + (t/is (some? (:deleted-at (get-object-row (:resource-id staged))))) + + (t/testing "releasing twice is a no-op, not an error" + (t/is (false? (js/release-input cfg context)))) + + (t/testing "releasing a job without a resource is a no-op too" + (t/is (nil? (js/release-input cfg (dissoc context :resource-id))))))) + +(t/deftest release-input-clears-the-reference-of-the-job + ;; a job that no longer owns its package must not keep pointing at a + ;; deleted object: the object and the pointer are dropped together + (let [cfg (make-cfg) + profile-id (:id (th/create-profile* 1)) + staged (put-artifact cfg profile-id "package-bytes") + job-id (uuid/next) + context {:id job-id + :resource-id (:resource-id staged) + :profile-id profile-id}] + + (th/db-insert! :job {:id job-id + :name "import-binfile" + :tenant (cf/get :tenant) + :queue "binfile" + :params (db/json {}) + :priority 100 + :max-retries 0 + :retry-num 0 + :status "running" + :profile-id profile-id + :resource-id (:resource-id staged) + :scheduled-at (ct/now) + :created-at (ct/now) + :modified-at (ct/now)}) + + (t/is (true? (js/release-input cfg context))) + + (t/testing "the object is marked as deleted" + (t/is (some? (:deleted-at (get-object-row (:resource-id staged)))))) + + (t/testing "and the job row no longer points at it" + (t/is (nil? (:resource-id (th/db-get :job {:id job-id}))))) + + (t/testing "releasing twice leaves the row alone" + (t/is (false? (js/release-input cfg context))) + (t/is (nil? (:resource-id (th/db-get :job {:id job-id}))))))) + +(t/deftest release-resource-releases-a-package-no-job-owns + ;; the creation path stages the package before the job exists: when the + ;; submit fails the package is released with no job id, so the object + ;; goes and no row may be touched by it + (let [cfg (make-cfg) + profile-id (:id (th/create-profile* 1)) + staged (put-artifact cfg profile-id "package-bytes") + job-id (uuid/next)] + (th/db-insert! :job {:id job-id + :name "import-binfile" + :tenant (cf/get :tenant) + :queue "binfile" + :params (db/json {}) + :priority 100 + :max-retries 0 + :retry-num 0 + :status "running" + :profile-id profile-id + :resource-id nil + :scheduled-at (ct/now) + :created-at (ct/now) + :modified-at (ct/now)}) + + (let [before (th/db-get :job {:id job-id})] + (t/is (true? (js/release-resource cfg (:resource-id staged)))) + + (t/testing "the object is marked as deleted" + (t/is (some? (:deleted-at (get-object-row (:resource-id staged)))))) + + (t/testing "and the unrelated job row is exactly as it was" + (t/is (= before (th/db-get :job {:id job-id}))))))) diff --git a/backend/test/backend_tests/jobs_test.clj b/backend/test/backend_tests/jobs_test.clj index 2aba405738..20675fed2b 100644 --- a/backend/test/backend_tests/jobs_test.clj +++ b/backend/test/backend_tests/jobs_test.clj @@ -27,6 +27,11 @@ (t/use-fixtures :each test-fixture) +(defn- raised + "Run `f` and answer with the exception it raised, or nil when it did not." + [f] + (try (f) nil (catch Throwable cause cause))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; JOB-DEF (plain handler + precompiled init-key) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -39,7 +44,8 @@ [cfg params] (when (::jobs/job-id cfg) (jobs/heartbeat cfg) - (jobs/heartbeat cfg :progress {:current 1 :stage "half"})) + (jobs/heartbeat cfg :progress {:stage :half + :counters {:work {:current 1 :total 2}}})) params) (def schema:echo-params @@ -101,7 +107,24 @@ WHERE job_id = ? AND kind = 'progress' ORDER BY created_at ASC, id ASC" job-id]) - (mapv #(db/decode-json-pgobject (:payload %))))) + (mapv #(jobs/decode-progress (:payload %))))) + +(defn- get-events + "Every event of a job, in insertion order." + [job-id] + (->> (th/db-exec! ["SELECT kind, payload FROM job_event + WHERE job_id = ? ORDER BY id ASC" job-id]) + (mapv (fn [{:keys [kind payload]}] + {:kind kind :payload (db/decode-json-pgobject payload)})))) + +(defn- get-outcomes + "Outcomes of the end events of a job, oldest first." + [job-id] + (mapv (comp :outcome :payload) (filter #(= "end" (:kind %)) (get-events job-id)))) + +(defn- get-kinds + [job-id] + (mapv :kind (get-events job-id))) (defn- fake-msgbus "A minimal msgbus that records every publication on the atom." @@ -159,48 +182,101 @@ (t/testing "no job row was created by the rejected submits" (t/is (= 0 (:cnt (th/db-exec-one! ["SELECT count(*) AS cnt FROM job"]))))))) +(t/deftest submit-persists-an-expiry-date + (let [cfg (make-cfg (get-job-defs)) + expires-at (ct/plus (ct/now) (ct/duration {:days 7}))] + + (t/testing "an explicit expiry is persisted as the retention of the job" + (let [job-id (jobs/submit cfg {::jobs/name :echo + ::jobs/params (make-params) + ::jobs/expires-at expires-at}) + row (jobs/get-job cfg job-id)] + (t/is (= (inst-ms expires-at) (inst-ms (:expires-at row)))))) + + (t/testing "without the option the column stays nil, as before" + (let [job-id (jobs/submit cfg {::jobs/name :echo + ::jobs/params (make-params)}) + row (jobs/get-job cfg job-id)] + (t/is (nil? (:expires-at row))))))) + +(t/deftest submit-rejects-an-invalid-expiry-date + (let [cfg (make-cfg (get-job-defs))] + (t/testing "a value that is not an instant is rejected before the insert" + (t/is (thrown? Exception + (jobs/submit cfg {::jobs/name :echo + ::jobs/params (make-params) + ::jobs/expires-at "not-an-instant"})))) + (t/testing "no job row was created by the rejected submit" + (t/is (= 0 (:cnt (th/db-exec-one! ["SELECT count(*) AS cnt FROM job"]))))))) + (t/deftest progress-report-validates-its-payload (let [cfg (make-cfg (get-job-defs)) job-id (jobs/submit cfg {::jobs/name :echo ::jobs/params (make-params)})] - (t/testing "current is mandatory" - (t/is (thrown? Exception - (jobs/heartbeat cfg :job-id job-id :progress {:total 10})))) - - (t/testing "unknown keys are rejected" + (t/testing "stage is mandatory" (t/is (thrown? Exception (jobs/heartbeat cfg :job-id job-id - :progress {:current 1 :step 1})))) + :progress {:counters {:work {:current 1}}})))) - (t/testing "current must not be negative" + (t/testing "stage must be a keyword, not a label" + (t/is (thrown? Exception + (jobs/heartbeat cfg :job-id job-id + :progress {:stage "work"})))) + + (t/testing "unknown keys are rejected, at the top and inside a counter" + (t/is (thrown? Exception + (jobs/heartbeat cfg :job-id job-id + :progress {:stage :work :step 1}))) + (t/is (thrown? Exception + (jobs/heartbeat cfg :job-id job-id + :progress {:stage :work + :counters {:work {:current 1 + :step 1}}})))) + + (t/testing "a counter current must not be negative" (t/is (thrown-with-msg? Exception #"negative" (jobs/heartbeat cfg :job-id job-id - :progress {:current -1})))) + :progress {:stage :work + :counters {:work {:current -1}}})))) - (t/testing "total must be positive" + (t/testing "a counter total must be positive" (t/is (thrown-with-msg? Exception #"positive" (jobs/heartbeat cfg :job-id job-id - :progress {:current 0 :total 0})))) + :progress {:stage :work + :counters {:work {:current 0 + :total 0}}})))) - (t/testing "current must not be greater than total" + (t/testing "a counter current must not be greater than its total" (t/is (thrown-with-msg? Exception #"not lower than current" (jobs/heartbeat cfg :job-id job-id - :progress {:current 5 :total 4})))) + :progress {:stage :work + :counters {:work {:current 5 + :total 4}}})))) - (t/testing "stage is limited to 250 characters" - (t/is (thrown-with-msg? Exception #"250" + (t/testing "keys are limited to a name and not a payload" + (t/is (thrown-with-msg? Exception #"64" (jobs/heartbeat cfg :job-id job-id - :progress {:current 1 - :stage (apply str (repeat 251 "x"))}))) + :progress {:stage (keyword (apply str (repeat 65 "x")))})))) + + (t/testing "a milestone without counters is accepted" + (t/is (= 2 (jobs/heartbeat cfg :job-id job-id :progress {:stage :relations})))) + + (t/testing "a milestone with counters is accepted and stored as it is" + (swap! jobs/progresses dissoc job-id) + (swap! jobs/heartbeats dissoc job-id) (t/is (= 2 (jobs/heartbeat cfg :job-id job-id - :progress {:current 1 - :stage (apply str (repeat 250 "x"))}))) - (t/is (= [{:current 1 :stage (apply str (repeat 250 "x"))}] + :progress {:stage :pages + :counters {:files {:current 2 :total 5} + :pages {:current 3 :total 8}}}))) + (t/is (= [{:stage :relations} + {:stage :pages + :counters {:files {:current 2 :total 5} + :pages {:current 3 :total 8}}}] (get-progresss job-id)))) (t/testing "a rejected report never reaches the durable log" - (t/is (= 1 (count (get-progresss job-id))))))) + (t/is (= 2 (count (get-progresss job-id))))))) (t/deftest progress-event-publishes-msgbus-for-profile-jobs (let [cfg (make-cfg (get-job-defs)) @@ -211,7 +287,8 @@ ::jobs/params (make-params) ::jobs/profile-id profile-id})] (t/is (pos? (jobs/heartbeat job-cfg :job-id job-id - :progress {:current 3 :total 7}))) + :progress {:stage :pages + :counters {:files {:current 3 :total 7}}}))) (t/testing "the event is published on the topic of the job profile" (t/is (= 1 (count @messages))) (let [{:keys [topic message]} (first @messages)] @@ -220,7 +297,9 @@ (t/is (= job-id (:job-id message))) (t/is (= profile-id (:profile-id message))) (t/is (= "progress" (:kind message))) - (t/is (= {:current 3 :total 7} (:payload message))) + (t/is (= {:stage :pages + :counters {:files {:current 3 :total 7}}} + (:payload message))) (t/is (some? (:event-id message))) (t/is (some? (:created-at message))))))) @@ -230,7 +309,7 @@ job-cfg (assoc cfg ::mbus/msgbus (fake-msgbus messages)) job-id (jobs/submit job-cfg {::jobs/name :echo ::jobs/params (make-params)})] - (t/is (pos? (jobs/heartbeat job-cfg :job-id job-id :progress {:current 1}))) + (t/is (pos? (jobs/heartbeat job-cfg :job-id job-id :progress {:stage :work}))) (t/testing "the event is stored but nothing is published" (t/is (= 1 (count (get-progresss job-id)))) (t/is (= [] @messages))))) @@ -245,7 +324,7 @@ (t/is (thrown-with-msg? Exception #"require ::mbus/msgbus" (jobs/heartbeat cfg :job-id job-id - :progress {:current 1} + :progress {:stage :work} ::jobs/force? true))) (t/is (= [] (get-progresss job-id)) "the event must not be stored without its notification")) @@ -254,7 +333,7 @@ (th/db-update! :job {:modified-at (ct/in-past {:minutes 5})} {:id job-id}) (swap! jobs/heartbeats dissoc job-id) (t/is (= 1 (jobs/heartbeat cfg :job-id job-id - :progress {:current 1}))) + :progress {:stage :work}))) (t/is (= [] (get-progresss job-id)))))) (t/deftest submit-validates-params-with-job-schema @@ -530,34 +609,67 @@ (t/is (= (inst-ms (:modified-at row1)) (inst-ms (:modified-at row2)))))))) -(t/deftest heartbeat-skips-terminal-states +(t/deftest heartbeat-interrupts-on-terminal-states (let [cfg (make-cfg (get-job-defs)) job-id (jobs/submit cfg {::jobs/name :echo ::jobs/params (make-params)})] - (t/testing "terminal states are never touched by heartbeat" + (t/testing "a terminal state raises the interrupt instead of touching the row" (th/db-update! :job {:status "completed" :modified-at (ct/in-past {:days 10})} {:id job-id}) (swap! @#'jobs/heartbeats dissoc job-id) - (let [before (jobs/get-job cfg job-id)] - (jobs/heartbeat cfg :job-id job-id) + (let [before (jobs/get-job cfg job-id) + cause (raised #(jobs/heartbeat cfg :job-id job-id))] + (t/is (= :interrupt (th/ex-type cause))) + (t/is (= :job-interrupted (th/ex-code cause))) + (t/is (= job-id (:job-id (ex-data cause)))) + (t/is (= "completed" (:status (ex-data cause)))) (t/is (= (inst-ms (:modified-at before)) (inst-ms (:modified-at (jobs/get-job cfg job-id))))))))) -(t/deftest progress-events-respect-throttle-and-skip-terminal-states +(t/deftest heartbeat-interrupts-when-the-row-is-gone + (let [cfg (make-cfg (get-job-defs)) + job-id (jobs/submit cfg {::jobs/name :echo + ::jobs/params (make-params)})] + (th/db-force-delete :job {:id job-id}) + (let [cause (raised #(jobs/heartbeat cfg :job-id job-id))] + (t/is (= :interrupt (th/ex-type cause))) + (t/is (= :job-interrupted (th/ex-code cause))) + (t/is (= "gone" (:status (ex-data cause))))))) + +(t/deftest a-transient-progress-failure-is-not-an-interrupt + "The handler path logs and ignores a progress insert that fails, but an + interrupt must not be swallowed with it: only an interrupt means the job + is gone." + (let [cfg (make-cfg (get-job-defs)) + job-id (jobs/submit cfg {::jobs/name :echo + ::jobs/params (make-params)})] + (swap! jobs/progresses dissoc job-id) + (swap! jobs/heartbeats dissoc job-id) + ;; the insert is the boundary that fails: the job itself is active, so + ;; the beat answers with the touch it did write instead of raising + (with-redefs-fn {#'jobs/report-progress (fn [& _] (throw (ex-info "boom" {})))} + #(t/is (= 1 (jobs/heartbeat cfg :job-id job-id :progress {:stage :work})))) + (t/is (= [] (get-progresss job-id))))) + +(t/deftest progress-events-respect-throttle-and-interrupt-on-terminal-states (let [cfg (make-cfg (get-job-defs)) job-id (jobs/submit cfg {::jobs/name :echo ::jobs/params (make-params)})] (t/testing "first progress report appends a progress event" (t/is (= 2 (jobs/heartbeat cfg :job-id job-id - :progress {:current 1 :total 10}))) - (t/is (= [{:current 1 :total 10}] (get-progresss job-id)))) + :progress {:stage :pages + :counters {:pages {:current 1 :total 10}}}))) + (t/is (= [{:stage :pages :counters {:pages {:current 1 :total 10}}}] + (get-progresss job-id)))) (t/testing "immediate second progress report is throttled" (t/is (= 0 (jobs/heartbeat cfg :job-id job-id - :progress {:current 2 :total 10}))) - (t/is (= [{:current 1 :total 10}] (get-progresss job-id))) + :progress {:stage :pages + :counters {:pages {:current 2 :total 10}}}))) + (t/is (= [{:stage :pages :counters {:pages {:current 1 :total 10}}}] + (get-progresss job-id))) (t/testing "after the throttle window elapses it appends again" (swap! @#'jobs/progresses @@ -565,9 +677,11 @@ (update-in m [job-id] #(ct/minus % (ct/duration {:seconds 2}))))) - (jobs/heartbeat cfg :job-id job-id :progress {:current 3 :total 10}) - (t/is (= [{:current 1 :total 10} - {:current 3 :total 10}] + (jobs/heartbeat cfg :job-id job-id + :progress {:stage :pages + :counters {:pages {:current 3 :total 10}}}) + (t/is (= [{:stage :pages :counters {:pages {:current 1 :total 10}}} + {:stage :pages :counters {:pages {:current 3 :total 10}}}] (get-progresss job-id)))))) (let [cfg (make-cfg (get-job-defs)) @@ -576,9 +690,11 @@ (t/testing "terminal states never get a progress event" (th/db-update! :job {:status "completed"} {:id job-id}) (swap! @#'jobs/progresses dissoc job-id) - (t/is (= 0 (jobs/heartbeat cfg :job-id job-id - :progress {:current 9}))) - (t/is (= [] (get-progresss job-id)))))) + (swap! @#'jobs/heartbeats dissoc job-id) + (let [cause (raised #(jobs/heartbeat cfg :job-id job-id + :progress {:stage :work}))] + (t/is (= :interrupt (th/ex-type cause))) + (t/is (= [] (get-progresss job-id))))))) (t/deftest throttle-prune-removes-stale-entries-keeps-fresh "When the throttle map exceeds prune-threshold, stale entries (older than @@ -609,7 +725,7 @@ (t/is (contains? state job-id-2) "fresh entry kept") (t/is (contains? state job-id-3) "new entry added")))))) -(t/deftest cancel-skips-running-and-terminal-jobs +(t/deftest cancel-skips-terminal-jobs (let [cfg (make-cfg (get-job-defs)) job-id (jobs/submit cfg {::jobs/name :echo ::jobs/params (make-params)})] @@ -617,27 +733,30 @@ (t/testing "pending job can be cancelled" (t/is (= 1 (jobs/cancel cfg job-id))) (t/is (= "cancelled" (:status (jobs/get-job cfg job-id)))) - + (t/is (= ["cancelled"] (get-outcomes job-id))) (t/testing "already cancelled job is not affected again" - (t/is (zero? (jobs/cancel cfg job-id)))))) + (t/is (zero? (jobs/cancel cfg job-id))) + (t/is (= ["cancelled"] (get-outcomes job-id)))))) + (doseq [status ["completed" "failed"]] + (t/testing (str status " job cannot be cancelled") + (let [cfg (make-cfg (get-job-defs)) + job-id (jobs/submit cfg {::jobs/name :echo + ::jobs/params (make-params)})] + (th/db-update! :job {:status status} {:id job-id}) + (t/is (zero? (jobs/cancel cfg job-id))) + (t/is (= status (:status (jobs/get-job cfg job-id)))) + (t/is (= [] (get-outcomes job-id))))))) - (let [cfg (make-cfg (get-job-defs)) - job-id (jobs/submit cfg {::jobs/name :echo - ::jobs/params (make-params)})] - (t/testing "running job cannot be cancelled" - (th/db-update! :job {:status "running"} {:id job-id}) - (t/is (zero? (jobs/cancel cfg job-id))) - (t/is (= "running" (:status (jobs/get-job cfg job-id))))))) - -(t/deftest cancel-affects-scheduled-and-retry-jobs +(t/deftest cancel-affects-pending-and-running-jobs (let [cfg (make-cfg (get-job-defs))] - (doseq [status ["scheduled" "retry"]] + (doseq [status ["scheduled" "retry" "running"]] (t/testing (str status " job can be cancelled") (let [job-id (jobs/submit cfg {::jobs/name :echo ::jobs/params (make-params)})] (th/db-update! :job {:status status} {:id job-id}) (t/is (= 1 (jobs/cancel cfg job-id))) - (t/is (= "cancelled" (:status (jobs/get-job cfg job-id))))))))) + (t/is (= "cancelled" (:status (jobs/get-job cfg job-id)))) + (t/is (= ["cancelled"] (get-outcomes job-id)))))))) (t/deftest get-user-status-maps-internal-statuses (t/are [status expected] (= expected (jobs/get-user-status status)) @@ -673,6 +792,32 @@ ::jobs/handler (fn [_context params] params)}}}))))) +(t/deftest job-def-carries-optional-family-and-resource-role + (let [job-def {::jobs/name :echo + ::jobs/family :export + ::jobs/resource-role :output + ::jobs/schema schema:echo-params + ::jobs/handler (fn [_context params] params) + ::jobs/decoder (sm/decoder schema:echo-params sm/json-transformer) + ::jobs/validator (sm/validator schema:echo-params)}] + + (t/testing "the metadata is accepted and readable from the registry" + (let [defs (-> (ig/init {::jobs/defs {:echo job-def}}) + (get ::jobs/defs)) + echo (jobs/get-job-def defs :echo)] + (t/is (= :export (::jobs/family echo))) + (t/is (= :output (::jobs/resource-role echo))))) + + (t/testing "a family is a keyword, not a free string" + (t/is (thrown? Exception + (ig/init {::jobs/defs {:echo (assoc job-def + ::jobs/family "export")}})))) + + (t/testing "a resource role is a keyword too" + (t/is (thrown? Exception + (ig/init {::jobs/defs {:echo (assoc job-def + ::jobs/resource-role "output")}})))))) + (t/deftest generic-schema-round-trip-preserves-type-sensitive-fields "For each registered job-def, verify that type-sensitive fields (uuids, insts) survive the JSON round-trip (db/json → decode). This catches the F2 class of @@ -742,25 +887,28 @@ (t/testing "progress reporting is a no-op when *job-id* is nil" (binding [jobs/*job-id* nil] - (t/is (nil? (jobs/heartbeat cfg :progress {:current 1})))) + (t/is (nil? (jobs/heartbeat cfg :progress {:stage :work})))) (t/is (= [] (get-progresss job-id)) "no progress event should be stored")))) -(t/deftest progress-noop-on-terminal-states +(t/deftest progress-interrupts-on-terminal-states "A progress report never stores an event when the job is already in a terminal state (completed, failed, cancelled, aborted): the row is locked and checked before the insert, so a report can never race a terminal - transition." + transition, and the beat raises the interrupt instead." (let [cfg (make-cfg (get-job-defs))] (doseq [status ["completed" "failed" "cancelled" "aborted"]] - (t/testing (str "progress is a no-op on " status " status") + (t/testing (str "progress interrupts on " status " status") (let [job-id (jobs/submit cfg {::jobs/name :echo ::jobs/params (make-params)})] (th/db-update! :job {:status status} {:id job-id}) - ;; reset the throttle so should-write? would allow the write + ;; reset the throttles so should-write? would allow the writes (swap! jobs/progresses dissoc job-id) + (swap! jobs/heartbeats dissoc job-id) (binding [jobs/*job-id* job-id] - (t/is (= 0 (jobs/heartbeat cfg :progress {:current 1})))) + (let [cause (raised #(jobs/heartbeat cfg :progress {:stage :work}))] + (t/is (= :interrupt (th/ex-type cause))) + (t/is (= status (:status (ex-data cause)))))) (t/is (= [] (get-progresss job-id)))))))) (t/deftest defs-halt-clears-module-registry @@ -797,8 +945,8 @@ (fn [{:keys [::db/conn]}] (jobs/heartbeat (assoc cfg ::db/conn conn) :job-id job-id - :progress {:current 1}))) - (t/is (= [{:current 1}] (get-progresss job-id))))) + :progress {:stage :work}))) + (t/is (= [{:stage :work}] (get-progresss job-id))))) (t/deftest progress-notification-survives-a-rolled-back-caller (let [cfg (make-cfg (get-job-defs)) @@ -814,9 +962,9 @@ (fn [{:keys [::db/conn]}] (jobs/heartbeat (assoc job-cfg ::db/conn conn) :job-id job-id - :progress {:current 1}))) + :progress {:stage :work}))) (t/testing "the event is stored" - (t/is (= [{:current 1}] (get-progresss job-id)))) + (t/is (= [{:stage :work}] (get-progresss job-id)))) (t/testing "and the notification went out with its own commit" (t/is (= 1 (count @messages))) (t/is (= "progress" (:kind (:message (first @messages)))))))) @@ -829,7 +977,7 @@ (t/is (thrown? Exception (jobs/heartbeat th/*pool* :job-id job-id - :progress {:current 1})))))) + :progress {:stage :work})))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -844,6 +992,7 @@ :tenant (cf/get :tenant) :queue "default" :label nil + :profile-id nil :resource-id nil :retry-num 0 :max-retries 3} @@ -851,7 +1000,7 @@ (t/deftest make-context-selects-exactly-the-agreed-keys (let [context (jobs/make-context (mk-row {}))] - (t/is (= #{:id :name :label :resource-id} (set (keys context)))) + (t/is (= #{:id :name :label :resource-id :profile-id} (set (keys context)))) (t/testing "the queue is a routing detail of the dispatcher, not context" (t/is (not (contains? context :queue)))) (t/testing "nor is the tenant that owns the queue" @@ -880,11 +1029,23 @@ (t/is (= resource-id (:resource-id (jobs/make-context (mk-row {:resource-id resource-id})))))))) +(t/deftest make-context-carries-the-job-owner + (t/testing "the key exists even when the job has no owner" + (let [context (jobs/make-context (mk-row {}))] + (t/is (contains? context :profile-id)) + (t/is (nil? (:profile-id context))))) + (t/testing "the owner of a user job is carried as is" + (let [profile-id (uuid/next)] + (t/is (= profile-id + (:profile-id (jobs/make-context (mk-row {:profile-id profile-id})))))))) + (t/deftest make-context-rejects-invalid-rows (t/testing "a missing id is rejected" (t/is (thrown? Exception (jobs/make-context (mk-row {:id nil}))))) (t/testing "a missing name is rejected" (t/is (thrown? Exception (jobs/make-context (mk-row {:name nil}))))) + (t/testing "a non uuid owner is rejected" + (t/is (thrown? Exception (jobs/make-context (mk-row {:profile-id "nope"}))))) (t/testing "a non uuid resource reference is rejected" (t/is (thrown? Exception (jobs/make-context (mk-row {:resource-id "nope"})))))) @@ -955,29 +1116,13 @@ "a job-id without a context still delivers a nil context") (t/testing "the job-id is what makes the durable writes reach the row" (t/is (pos? (jobs/heartbeat cfg :job-id job-id - :progress {:current 1}))) - (t/is (= [{:current 1}] (get-progresss job-id)))))) + :progress {:stage :work}))) + (t/is (= [{:stage :work}] (get-progresss job-id)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; LIFECYCLE EVENTS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- get-events - "Every event of a job, in insertion order." - [job-id] - (->> (th/db-exec! ["SELECT kind, payload FROM job_event - WHERE job_id = ? ORDER BY id ASC" job-id]) - (mapv (fn [{:keys [kind payload]}] - {:kind kind :payload (db/decode-json-pgobject payload)})))) - -(defn- get-kinds - [job-id] - (mapv :kind (get-events job-id))) - -(defn- get-outcomes - [job-id] - (mapv (comp :outcome :payload) (filter #(= "end" (:kind %)) (get-events job-id)))) - (defn- mk-running "A job already claimed by a worker, ready for a terminal transition." [cfg] @@ -1269,6 +1414,23 @@ (t/is (nil? (:resource-id row))) (t/is (= "running" (:status row)))))))) +(t/deftest insert-event-requires-a-transaction + ;; The event insert is only valid inside the transaction that owns the + ;; job row: outside one the assert fires instead of writing. Like every + ;; assert-based guard, this only holds with the backend asserts flag. + (t/is (true? *assert*) + "run the suite with PENPOT_FLAGS=\"enable-backend-asserts\"") + (let [cfg (make-cfg (get-job-defs)) + job-id (mk-running cfg)] + (t/testing "a pool cfg with no transaction raises" + (t/is (thrown? AssertionError + (@#'jobs/insert-event cfg job-id "progress" {:stage :half})))) + (t/testing "a bare connection with no transaction raises too" + (db/run! cfg (fn [conn-cfg] + (t/is (thrown? AssertionError + (@#'jobs/insert-event conn-cfg job-id "progress" + {:stage :half})))))))) + (t/deftest complete-associates-a-resource-id-once (let [cfg (make-cfg (get-job-defs)) resource-id (uuid/next)] @@ -1393,10 +1555,17 @@ (let [cfg (make-cfg (get-job-defs)) job-id (mk-running cfg)] (t/testing "a progress report and a completion of the same job, at once" - (let [[_writes completed] (race #(jobs/heartbeat cfg :job-id job-id - :progress {:current 1} - ::jobs/force? true) - #(jobs/complete cfg :job-id job-id :result {:v 1}))] + ;; the report either lands before the end or loses the race and gets + ;; the interrupt: a beat on a job that is no longer active raises + (let [report (fn [] + (if (some? (raised #(jobs/heartbeat cfg :job-id job-id + :progress {:stage :work} + ::jobs/force? true))) + :interrupted + :reported)) + [outcome completed] (race report #(jobs/complete cfg :job-id job-id :result {:v 1}))] + (t/is (contains? #{:interrupted :reported} outcome) + "the report lands or gets the interrupt, never anything else") (t/is (= 1 completed) "the completion always wins, it owns the row") (t/testing "the report either lost the race or was stored before the end" (let [kinds (get-kinds job-id)] @@ -1406,10 +1575,11 @@ (t/is (<= 2 (count kinds) 3) "at most the one report that made it in before the end")))) (t/testing "a job that ended is not resurrected by a late report" - (let [before (get-kinds job-id)] - (t/is (zero? (jobs/heartbeat cfg :job-id job-id - :progress {:current 2} - ::jobs/force? true))) + (let [before (get-kinds job-id) + cause (raised #(jobs/heartbeat cfg :job-id job-id + :progress {:stage :work} + ::jobs/force? true))] + (t/is (= :interrupt (th/ex-type cause))) (t/is (= before (get-kinds job-id)) "the late report stored nothing")))))) (t/deftest progress-event-reaches-the-cfg-of-its-own-callback @@ -1426,9 +1596,9 @@ ::jobs/params (make-params) ::jobs/profile-id profile-id})] (t/is (pos? (jobs/heartbeat job-cfg :job-id job-id - :progress {:current 1}))) + :progress {:stage :work}))) (t/testing "the event was stored" - (t/is (= [{:current 1}] (get-progresss job-id)))) + (t/is (= [{:stage :work}] (get-progresss job-id)))) (t/testing "and the msgbus on that cfg was reachable from the callback" (t/is (= 1 (count @messages))) (t/is (= "progress" (:kind (:message (first @messages)))))))) diff --git a/backend/test/backend_tests/rpc/commands/jobs_test.clj b/backend/test/backend_tests/rpc/commands/jobs_test.clj new file mode 100644 index 0000000000..a03b5a9ca4 --- /dev/null +++ b/backend/test/backend_tests/rpc/commands/jobs_test.clj @@ -0,0 +1,717 @@ +;; 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 SUBSIDIARY SL + +(ns backend-tests.rpc.commands.jobs-test + "The creation of a user-facing job: what it answers, what it freezes and + what it refuses before anything is stored." + (:require + [app.binfile.common :as bfc] + [app.binfile.v3 :as v3] + [app.common.exceptions :as ex] + [app.common.time :as ct] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.jobs :as jobs] + [app.loggers.audit :as-alias audit] + [app.rpc :as-alias rpc] + [app.rpc.climit :as-alias climit] + [app.rpc.commands.jobs :as cmd-jobs] + [app.rpc.quotes :as-alias quotes] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [backend-tests.helpers :as th] + [clojure.java.io :as jio] + [clojure.test :as t] + [cuerdas.core :as str] + [datoteka.fs :as fs] + [datoteka.io :as io] + [mockery.core :refer [with-mocks]]) + (:import + java.util.zip.ZipEntry + java.util.zip.ZipFile + java.util.zip.ZipOutputStream)) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each (th/serial th/database-reset th/clean-storage)) + +(def ^:private fixture + "A real penpot export, the same fixture the binfile tests read." + "backend_tests/test_files/svg-attrs-camel-case.penpot") + +(defn- import-fixture! + "A file in the default project of the profile, imported from the + fixture through the core." + [profile] + (-> th/*system* + (assoc ::bfc/project-id (:default-project-id profile)) + (assoc ::bfc/profile-id (:id profile)) + (assoc ::bfc/team-id (:default-team-id profile)) + (assoc ::bfc/input (-> fixture io/resource jio/file)) + (v3/import-files!))) + +(defn- create-export-binfile-job + [profile-id file-ids & {:as extra}] + (th/command! (merge {::th/type :create-export-binfile-job + ::rpc/profile-id profile-id + :params {:file-ids file-ids + :export-type :detach-libraries}} + extra))) + +(defn- count-jobs + [] + (count (th/db-exec! ["SELECT id FROM job"]))) + +(defn- count-storage-objects + [] + (count (th/db-exec! ["SELECT id FROM storage_object"]))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TESTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest create-export-binfile-job-answers-with-a-pending-job + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + before (count-storage-objects) + out (create-export-binfile-job (:id profile) #{file-id})] + + (t/is (th/success? out)) + + (let [result (:result out) + job (jobs/get-job th/*system* (:id result))] + + (t/testing "the answer is enough to follow the job" + (t/is (uuid? (:id result))) + (t/is (= "pending" (:status result))) + (t/is (= "export-binfile" (:name result))) + (t/is (ct/inst? (:created-at result))) + (t/is (pos? (- (inst-ms (:expires-at result)) (inst-ms (ct/now)))))) + + (t/testing "and the row freezes what the job will do" + (let [job-def (jobs/get-job-def (::jobs/defs th/*system*) :export-binfile) + params (jobs/decode-params job-def (:params job))] + (t/is (= "new" (:status job))) + (t/is (= "binfile" (:queue job))) + (t/is (= 0 (:max-retries job))) + (t/is (= (:id profile) (:profile-id job))) + (t/is (= #{file-id} (:file-ids params))) + (t/is (= :detach-libraries (:export-type params))))) + + (t/testing "creating a job does not run it" + ;; no runner is running here, so a call that waited for one would + ;; hang instead of answering: seeing the job pending and no + ;; artifact produced is what proves the call does not wait + (t/is (= before (count-storage-objects))))))) + +(t/deftest create-export-binfile-job-requires-the-export-type + ;; the type is part of the contract of the job: a caller that does not + ;; say how to handle the libraries gets an error, not a package it did + ;; not ask for + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + out (th/command! {::th/type :create-export-binfile-job + ::rpc/profile-id (:id profile) + :params {:file-ids #{file-id}}})] + + (t/is (not (th/success? out))) + (t/is (= :params-validation (th/ex-code (:error out)))) + (t/testing "and nothing was created" + (t/is (zero? (count-jobs)))))) + +(t/deftest create-export-binfile-job-requires-at-least-one-file + (let [profile (th/create-profile* 1) + out (create-export-binfile-job (:id profile) #{})] + + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))) + (t/is (= :no-files-to-export (th/ex-code (:error out)))) + (t/testing "and nothing was created" + (t/is (zero? (count-jobs)))))) + +(t/deftest create-export-binfile-job-checks-the-read-permission + (let [owner (th/create-profile* 1) + other (th/create-profile* 2) + file-id (first (:file-ids (import-fixture! owner))) + out (create-export-binfile-job (:id other) #{file-id})] + + (t/testing "a file the caller cannot read is not exported" + (t/is (not (th/success? out))) + (t/is (= :not-found (th/ex-type (:error out))))) + + (t/testing "and no job was created for it" + (t/is (zero? (count-jobs)))))) + +(t/deftest create-export-binfile-job-enforces-the-quote + (with-mocks [mock {:target 'app.config/get + :return (th/config-get-mock + {:quotes-export-jobs-per-profile 1})}] + + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile)))] + + (t/testing "the first job fits in the quote" + (t/is (th/success? (create-export-binfile-job (:id profile) #{file-id}))) + (t/is (= 1 (count-jobs)))) + + (t/testing "the one that would go over it is refused" + (let [out (create-export-binfile-job (:id profile) #{file-id})] + (t/is (not (th/success? out))) + (t/is (= :restriction (th/ex-type (:error out)))) + (t/is (= :max-quote-reached (th/ex-code (:error out)))) + (t/is (= "export-jobs-per-profile" (:target (ex-data (:error out))))))) + + (t/testing "so the quote is what stopped the second job" + (t/is (= 1 (count-jobs))))))) + +(t/deftest create-export-binfile-job-adds-the-job-id-to-the-audit-props + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + out (create-export-binfile-job (:id profile) #{file-id}) + props (::audit/props (meta (:result out)))] + + (t/testing "the audit of the call points at the job, not at its files" + (t/is (= (:id (:result out)) (:job-id props))) + (t/is (= 1 (:files props))) + (t/is (= "detach-libraries" (:export-type props))) + (t/is (= #{:job-id :files :export-type} (set (keys props))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; IMPORT +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- tempfile-with + "A temporary file holding `content`, as the http layer would leave one + for a plain upload." + [content] + (let [path (fs/create-tempfile :dir tmp/default-tmp-dir + :prefix "test-upload-")] + (with-open [out (io/output-stream path)] + (io/write* out (.getBytes ^String content "UTF-8"))) + path)) + +(defn- upload-chunk + "One chunk of the fixture, as the upload command takes it. `content` + replaces the fixture with something that is no package at all." + [& {:keys [content]}] + (if (some? content) + {:filename "package.penpot" + :path (tempfile-with content) + :mtype "application/zip" + :size (count content)} + (let [source (-> fixture io/resource jio/file)] + {:filename "package.penpot" + :path (th/tempfile fixture) + :mtype "application/zip" + :size (.length source)}))) +(defn- upload-chunked! + "Upload one chunk and return the session id. By default the chunk is the + fixture; `:path` uploads another file and `:content` uploads a string + that is no package at all." + [profile & {:keys [content path]}] + (let [session-id (-> (th/command! {::th/type :create-upload-session + ::rpc/profile-id (:id profile) + :total-chunks 1}) + :result :session-id) + out (th/command! {::th/type :upload-chunk + ::rpc/profile-id (:id profile) + :session-id session-id + :index 0 + :content (if (some? path) + (let [size (.length (jio/file path))] + {:filename "package.penpot" + :path path + :mtype "application/zip" + :size size}) + (upload-chunk :content content))})] + (t/is (nil? (:error out))) + session-id)) + +(defn- create-import-binfile-job + [profile-id project-id & {:keys [upload-id] :as extra}] + (th/command! (merge {::th/type :create-import-binfile-job + ::rpc/profile-id profile-id + :params {:project-id project-id + :name "imported"} + :upload-id upload-id} + extra))) + +(defn- session-row + [session-id] + (th/db-get :upload-session {:id session-id} {::db/remove-deleted false})) + +(defn- package-with-refer + "The fixture with the key of its manifest renamed to the old spelling, + `refer`, and keeping the value it already had." + [] + (let [source (-> fixture io/resource jio/file) + target (fs/create-tempfile :dir tmp/default-tmp-dir + :prefix "test-refer-")] + (with-open [zip (ZipFile. source) + out (ZipOutputStream. (io/output-stream target))] + (doseq [entry (enumeration-seq (.entries zip))] + (.putNextEntry out (ZipEntry. (.getName entry))) + (when-not (.isDirectory entry) + (let [content (slurp (.getInputStream zip entry)) + content (if (= "manifest.json" (.getName entry)) + (str/replace content #"\"referer\"(?=\s*:)" "\"refer\"") + content)] + (.write out (.getBytes content "UTF-8")))) + (.closeEntry out))) + target)) + +(defn- manifest-of + "The manifest of a package, as the reader of the import sees it." + [path] + (v3/get-manifest th/*system* path)) + +(defn- live-storage-objects + [] + (count (th/db-exec! ["SELECT id FROM storage_object WHERE deleted_at IS NULL"]))) + +(defn- session-row + [session-id] + (th/db-get :upload-session {:id session-id} {::db/remove-deleted false})) + +(defn- package-with-refer + "The fixture with the key of its manifest renamed to the old spelling, + `refer`, and keeping the value it already had." + [] + (let [source (-> fixture io/resource jio/file) + target (fs/create-tempfile :dir tmp/default-tmp-dir + :prefix "test-refer-")] + (with-open [zip (ZipFile. source) + out (ZipOutputStream. (io/output-stream target))] + (doseq [entry (enumeration-seq (.entries zip))] + (.putNextEntry out (ZipEntry. (.getName entry))) + (when-not (.isDirectory entry) + (let [content (slurp (.getInputStream zip entry)) + content (if (= "manifest.json" (.getName entry)) + (str/replace content #"\"referer\"(?=\s*:)" "\"refer\"") + content)] + (.write out (.getBytes content "UTF-8")))) + (.closeEntry out))) + target)) + +(defn- manifest-of + "The manifest of a package, as the reader of the import sees it." + [path] + (v3/get-manifest th/*system* path)) + +(defn- assembled-tempfiles + "The temporary files the chunk assembly leaves behind. The copies this + test uploads are its own business." + [] + (->> (.list (jio/file tmp/default-tmp-dir)) + (filter #(str/starts-with? % "penpot.chunked-upload.")) + (seq))) + +(t/deftest create-import-binfile-job-audits-the-old-spelling-of-the-manifest-tool + (let [profile (th/create-profile* 1) + package (package-with-refer) + expected (:referer (manifest-of (-> fixture io/resource jio/file))) + session-id (upload-chunked! profile :path package) + out (create-import-binfile-job (:id profile) (:default-project-id profile) + :upload-id session-id) + props (::audit/props (meta (:result out)))] + + (t/testing "the package names its tool with the old spelling only" + (t/is (some? expected)) + (t/is (nil? (:referer (manifest-of package)))) + (t/is (= expected (:refer (manifest-of package))))) + + (t/testing "and the audit carries that value anyway" + (t/is (th/success? out)) + (t/is (= expected (:referer props)))))) + +(t/deftest create-import-binfile-job-limits-its-own-concurrency + ;; the command assembles and stores the package inside the request, so + ;; it declares the same kind of limit the legacy import command has + (let [[mdata _] (get (::rpc/methods th/*system*) :create-import-binfile-job)] + (t/is (= [[:create-import-binfile-job/by-profile ::rpc/profile-id] + [:create-import-binfile-job/global]] + (::climit/id mdata))))) + +(t/deftest create-export-binfile-job-accepts-the-params-of-a-json-client + ;; a JSON body carries text and lists, not uuids or sets: the command + ;; has to read them with the decoder of the job-def, like the runner + ;; does when it reads a job from its row + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + out (th/command! {::th/type :create-export-binfile-job + ::rpc/profile-id (:id profile) + :params {:file-ids [(str file-id)] + :export-type "detach-libraries"}})] + + (t/is (th/success? out)) + + (let [job-def (jobs/get-job-def (::jobs/defs th/*system*) :export-binfile) + params (jobs/decode-params job-def + (:params (jobs/get-job th/*system* + (:id (:result out)))))] + (t/is (= #{file-id} (:file-ids params))) + (t/is (= :detach-libraries (:export-type params)))))) + +(t/deftest create-import-binfile-job-accepts-the-params-of-a-json-client + (let [profile (th/create-profile* 1) + session-id (upload-chunked! profile) + out (th/command! {::th/type :create-import-binfile-job + ::rpc/profile-id (:id profile) + :params {:project-id (str (:default-project-id profile)) + :name "imported"} + :upload-id session-id})] + + (t/is (th/success? out)))) + +(t/deftest create-import-binfile-job-never-takes-a-path-from-the-caller + (let [profile (th/create-profile* 1) + out (create-import-binfile-job (:id profile) (:default-project-id profile) + :file {:filename "package.penpot" + :path (jio/file "/etc/passwd") + :mtype "application/zip" + :size 0})] + + (t/testing "a plain upload is not a way to name a package" + (t/is (not (th/success? out))) + (t/is (= :params-validation (th/ex-code (:error out))))) + + (t/testing "and no job is created" + (t/is (zero? (count-jobs)))))) + +(t/deftest create-import-binfile-job-answers-with-a-pending-job + (let [profile (th/create-profile* 1) + out (create-import-binfile-job (:id profile) (:default-project-id profile) + :upload-id (upload-chunked! profile))] + + (t/is (th/success? out)) + + (let [result (:result out) + job (jobs/get-job th/*system* (:id result)) + staged (sto/get-object (:app.storage/storage th/*system*) (:resource-id job))] + + (t/testing "the answer is enough to follow the job" + (t/is (uuid? (:id result))) + (t/is (= "pending" (:status result))) + (t/is (= "import-binfile" (:name result)))) + + (t/testing "the row points at the package the job will consume" + (t/is (some? (:resource-id job))) + (t/is (= "binfile" (:queue job))) + (t/is (= sto/job-resource-bucket (:bucket (meta staged)))) + (t/is (= (:id profile) (:profile-id (meta staged))))) + + (t/testing "and freezes the destination and the manifest metadata" + (let [job-def (jobs/get-job-def (::jobs/defs th/*system*) :import-binfile) + params (jobs/decode-params job-def (:params job))] + (t/is (= (:default-project-id profile) (:project-id params))) + (t/is (= "imported" (:name params))) + (t/is (= 3 (:version params))) + (t/is (string? (:generated-by params))))) + + (t/testing "the audit points at the job and the manifest, not the package" + (let [props (::audit/props (meta (:result out)))] + (t/is (= (:id result) (:job-id props))) + (t/is (= #{:job-id :generated-by :referer} (set (keys props))))))))) + +(t/deftest create-import-binfile-job-assembles-the-chunks-of-its-upload + (let [profile (th/create-profile* 1) + session-id (upload-chunked! profile) + out (create-import-binfile-job (:id profile) (:default-project-id profile) + :upload-id session-id)] + + (t/is (th/success? out)) + + (t/testing "the assembled package is the resource of the job" + (let [job (jobs/get-job th/*system* (:id (:result out)))] + (t/is (some? (:resource-id job))))) + + (t/testing "the upload session is consumed" + (t/is (some? (:deleted-at (session-row session-id))))) + + (t/testing "and the temporary file of the assembly is gone" + (t/is (empty? (assembled-tempfiles)))))) + +(t/deftest create-import-binfile-job-rejects-an-incomplete-upload + (let [profile (th/create-profile* 1) + session-id (-> (th/command! {::th/type :create-upload-session + ::rpc/profile-id (:id profile) + :total-chunks 2}) + :result :session-id) + out (create-import-binfile-job (:id profile) (:default-project-id profile) + :upload-id session-id)] + + (t/is (not (th/success? out))) + (t/is (= :missing-chunks (th/ex-code (:error out)))) + (t/is (zero? (count-jobs))))) + +(t/deftest create-import-binfile-job-refuses-an-upload-of-another-profile + (let [owner (th/create-profile* 1) + other (th/create-profile* 2) + session-id (upload-chunked! owner) + out (create-import-binfile-job (:id other) (:default-project-id other) + :upload-id session-id)] + + (t/testing "the upload belongs to the profile that made it" + (t/is (not (th/success? out))) + (t/is (= :not-found (th/ex-type (:error out))))) + + (t/testing "and it is not consumed by the attempt" + (t/is (nil? (:deleted-at (session-row session-id))))) + + (t/testing "nor is any job created" + (t/is (zero? (count-jobs)))))) + +(t/deftest create-import-binfile-job-checks-the-edition-permission + (let [owner (th/create-profile* 1) + other (th/create-profile* 2) + session-id (upload-chunked! other) + out (create-import-binfile-job (:id other) (:default-project-id owner) + :upload-id session-id)] + + (t/testing "a job that cannot edit the destination is refused" + (t/is (not (th/success? out)))) + + (t/testing "before the upload is assembled" + (t/is (nil? (:deleted-at (session-row session-id))))) + + (t/testing "and no job is created" + (t/is (zero? (count-jobs)))))) + +(t/deftest create-import-binfile-job-refuses-a-version-that-does-not-exist + (let [profile (th/create-profile* 1) + session-id (upload-chunked! profile) + ;; the chunks of the upload are alive: the GC reclaims them later + before (live-storage-objects) + out (create-import-binfile-job (:id profile) (:default-project-id profile) + :params {:project-id (:default-project-id profile) + :name "imported" + :version 2} + :upload-id session-id)] + + (t/is (not (th/success? out))) + (t/is (= :params-validation (th/ex-code (:error out)))) + (t/is (zero? (count-jobs))) + + (t/testing "no package was staged for it" + (t/is (= before (live-storage-objects)))) + + (t/testing "and the upload is not consumed by the rejection" + (t/is (nil? (:deleted-at (session-row session-id))))) + + (t/testing "so nothing had to be assembled either" + (t/is (empty? (assembled-tempfiles)))))) + +(t/deftest create-import-binfile-job-rejects-malformed-params-before-assembling + ;; the shape of the caller params is checked before the chunks are + ;; assembled: a name over the limit never copies the package to disk + (let [profile (th/create-profile* 1) + session-id (upload-chunked! profile) + before (live-storage-objects) + out (with-redefs [cmd-jobs/assemble-upload + (fn [& _] + (throw (ex-info "assembled" {})))] + (create-import-binfile-job (:id profile) (:default-project-id profile) + :params {:project-id (:default-project-id profile) + :name (apply str (repeat 251 "x"))} + :upload-id session-id))] + + (t/is (not (th/success? out))) + (t/is (= :params-validation (th/ex-code (:error out)))) + (t/is (zero? (count-jobs))) + + (t/testing "no package was staged for it" + (t/is (= before (live-storage-objects)))) + + (t/testing "so nothing had to be assembled either" + (t/is (empty? (assembled-tempfiles)))))) + +(t/deftest create-import-binfile-job-leaves-no-temporary-when-the-package-is-corrupt + (let [profile (th/create-profile* 1) + ;; a header that says "version 3" (the zip magic) over bytes that + ;; are not a zip at all: what fails is reading the manifest, once + ;; the chunks have already been assembled + session-id (upload-chunked! profile + {:content (str "PK\u0003\u0004" "not a zip")}) + before (live-storage-objects) + out (create-import-binfile-job (:id profile) (:default-project-id profile) + :upload-id session-id)] + + (t/is (not (th/success? out))) + (t/is (zero? (count-jobs))) + + (t/testing "no package was staged" + (t/is (= before (live-storage-objects)))) + + (t/testing "and the file assembled from the chunks is gone" + (t/is (empty? (assembled-tempfiles)))))) + +(t/deftest create-import-binfile-job-releases-the-package-when-the-submit-fails + (let [profile (th/create-profile* 1) + session-id (upload-chunked! profile) + ;; the chunks of the upload are alive: the GC reclaims them later + before (live-storage-objects) + out (with-redefs [jobs/submit (fn [& _] + (ex/raise :type :internal + :code :boom + :hint "cannot create the job"))] + (create-import-binfile-job (:id profile) (:default-project-id profile) + :upload-id session-id))] + + (t/testing "the failure reaches the caller" + (t/is (not (th/success? out)))) + + (t/testing "the staged package does not stay alive" + (t/is (= before (live-storage-objects)))) + + (t/testing "and no job row was left behind" + (t/is (zero? (count-jobs)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; READ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- get-job! + [profile-id job-id] + (th/command! {::th/type :get-job + ::rpc/profile-id profile-id + :id job-id})) + +(defn- claim-job! + "Drive a pending job to `running`, as the dispatcher would: the terminal + writers only accept a job that is being executed." + [job-id] + (jobs/claim th/*system* job-id + (:scheduled-at (th/db-get :job {:id job-id} :id :scheduled-at)))) + +(t/deftest get-job-answers-with-the-state-of-the-job + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (:id (:result (create-export-binfile-job (:id profile) #{file-id}))) + out (get-job! (:id profile) job-id)] + + (t/is (th/success? out)) + (let [result (:result out)] + (t/is (= job-id (:id result))) + (t/is (= "pending" (:status result))) + (t/is (= "export-binfile" (:name result))) + (t/is (ct/inst? (:created-at result))) + (t/is (ct/inst? (:expires-at result))) + + (t/testing "a job that has not finished has nothing to report yet" + (t/is (not (contains? result :result))) + (t/is (not (contains? result :error))))))) + +(t/deftest get-job-answers-with-the-result-of-a-finished-job + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (:id (:result (create-export-binfile-job (:id profile) #{file-id})))] + + (t/is (pos? (claim-job! job-id))) + (t/is (pos? (jobs/complete th/*system* :job-id job-id + :result {:value 42 :file-ids [file-id]}))) + + (let [result (:result (get-job! (:id profile) job-id))] + (t/is (= "completed" (:status result))) + (t/testing "the result comes back as the map the handler produced" + ;; the row holds plain json, so a uuid comes back as the text it is + (t/is (= {:value 42 :file-ids [(str file-id)]} (:result result)))) + (t/is (not (contains? result :error)))))) + +(t/deftest get-job-answers-with-the-error-of-a-failed-job + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (:id (:result (create-export-binfile-job (:id profile) #{file-id})))] + + (t/is (pos? (claim-job! job-id))) + (t/is (pos? (jobs/fail th/*system* job-id {:type :internal + :code :boom + :hint "boom"}))) + + (let [result (:result (get-job! (:id profile) job-id))] + (t/is (= "failed" (:status result))) + (t/testing "the error is the public summary, with its keywords back" + (t/is (= {:type :internal :code :boom :hint "boom"} (:error result)))) + (t/testing "and it carries no result" + (t/is (not (contains? result :result))))))) + +(t/deftest get-job-hides-a-job-of-another-profile + (let [owner (th/create-profile* 1) + other (th/create-profile* 2) + file-id (first (:file-ids (import-fixture! owner))) + job-id (:id (:result (create-export-binfile-job (:id owner) #{file-id})))] + + (t/testing "the owner reads it" + (t/is (th/success? (get-job! (:id owner) job-id)))) + + (t/testing "another profile gets the answer of a job that does not exist" + (let [out (get-job! (:id other) job-id)] + (t/is (not (th/success? out))) + (t/is (= :not-found (th/ex-type (:error out)))))) + + (t/testing "and so does an unknown id" + (let [out (get-job! (:id owner) (uuid/next))] + (t/is (not (th/success? out))) + (t/is (= :not-found (th/ex-type (:error out)))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; CANCEL +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- cancel-job + [profile-id job-id] + (th/command! {::th/type :cancel-job + ::rpc/profile-id profile-id + :id job-id})) + +(t/deftest cancel-job-cancels-a-running-job + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (:id (:result (create-export-binfile-job (:id profile) #{file-id})))] + + (t/is (pos? (claim-job! job-id))) + + (let [out (cancel-job (:id profile) job-id)] + (t/is (th/success? out)) + (t/testing "the answer names the cancelled job" + (t/is (= job-id (:id (:result out)))) + (t/is (= "cancelled" (:status (:result out))))) + (t/testing "and the row is cancelled" + (t/is (= "cancelled" (:status (jobs/get-job th/*system* job-id)))))))) + +(t/deftest cancel-job-hides-a-job-of-another-profile + (let [owner (th/create-profile* 1) + other (th/create-profile* 2) + file-id (first (:file-ids (import-fixture! owner))) + job-id (:id (:result (create-export-binfile-job (:id owner) #{file-id})))] + + (t/testing "another profile gets the answer of a job that does not exist" + (let [out (cancel-job (:id other) job-id)] + (t/is (not (th/success? out))) + (t/is (= :not-found (th/ex-type (:error out)))))) + + (t/testing "and the job keeps running untouched" + (t/is (pos? (claim-job! job-id))) + (t/is (= "running" (:status (jobs/get-job th/*system* job-id))))))) + +(t/deftest cancel-job-answers-a-finished-job-without-error + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (:id (:result (create-export-binfile-job (:id profile) #{file-id})))] + + (t/is (pos? (claim-job! job-id))) + (t/is (pos? (jobs/complete th/*system* :job-id job-id + :result {:value 42}))) + + (t/testing "cancelling what is already over answers its state" + (let [out (cancel-job (:id profile) job-id)] + (t/is (th/success? out)) + (t/is (= "completed" (:status (:result out)))))))) + +(t/deftest cancel-job-rejects-a-malformed-id + (let [profile (th/create-profile* 1) + out (cancel-job (:id profile) "nope")] + (t/is (not (th/success? out))) + (t/is (= :validation (th/ex-type (:error out)))))) diff --git a/backend/test/backend_tests/rpc_management_jobs_test.clj b/backend/test/backend_tests/rpc_management_jobs_test.clj index d129e9687c..9ff0882e68 100644 --- a/backend/test/backend_tests/rpc_management_jobs_test.clj +++ b/backend/test/backend_tests/rpc_management_jobs_test.clj @@ -74,7 +74,7 @@ WHERE job_id = ? AND kind = 'progress' ORDER BY created_at ASC, id ASC" id]) - (mapv #(db/decode-json-pgobject (:payload %))))) + (mapv #(jobs/decode-progress (:payload %))))) (defn- mgmt [type params] @@ -138,17 +138,20 @@ (let [job-id (mk-job {}) _ (jobs/claim (make-cfg) job-id (:scheduled-at (th/db-get :job {:id job-id} :id :scheduled-at))) out (mgmt :report-job-progress {:job-id job-id - :progress {:total 100 :current 50}})] + :progress {:stage :pages + :counters {:pages {:current 50 :total 100}}}})] (t/is (nil? (:error out))) (t/is (= {:action :run} (:result out))) - (t/is (= [{:total 100 :current 50}] (get-progresss job-id))) + (t/is (= [{:stage :pages :counters {:pages {:current 50 :total 100}}}] + (get-progresss job-id))) (t/testing "the job row has no progress column anymore" (t/is (not (contains? (get-row job-id) :progress)))))) (t/deftest report-job-progress-noop-on-terminal-row (let [job-id (mk-job {:status "completed"}) out (mgmt :report-job-progress {:job-id job-id - :progress {:total 100 :current 100}})] + :progress {:stage :pages + :counters {:pages {:current 100 :total 100}}}})] (t/is (nil? (:error out))) (t/is (= {:action :skip} (:result out))) (t/is (= [] (get-progresss job-id))))) @@ -157,25 +160,27 @@ (let [job-id (mk-job {}) _ (jobs/claim (make-cfg) job-id (:scheduled-at (th/db-get :job {:id job-id} :id :scheduled-at))) _ (mgmt :report-job-progress {:job-id job-id - :progress {:total 10 :current 3}}) + :progress {:stage :pages + :counters {:pages {:current 3 :total 10}}}}) _ (mgmt :report-job-progress {:job-id job-id - :progress {:total 10 :current 4}})] + :progress {:stage :pages + :counters {:pages {:current 4 :total 10}}}})] (t/testing "the second immediate report is not throttled away" - (t/is (= [{:total 10 :current 3} - {:total 10 :current 4}] + (t/is (= [{:stage :pages :counters {:pages {:current 3 :total 10}}} + {:stage :pages :counters {:pages {:current 4 :total 10}}}] (get-progresss job-id)))))) (t/deftest report-job-progress-validates-the-payload (let [job-id (mk-job {}) _ (jobs/claim (make-cfg) job-id (:scheduled-at (th/db-get :job {:id job-id} :id :scheduled-at)))] - (t/testing "current is mandatory" + (t/testing "stage is mandatory" (t/is (= :validation (th/ex-type (:error (mgmt :report-job-progress {:job-id job-id - :progress {:total 10}})))))) + :progress {:counters {:work {:current 1}}}})))))) (t/testing "no extra key is accepted" (t/is (= :validation (th/ex-type (:error (mgmt :report-job-progress {:job-id job-id - :progress {:current 1 + :progress {:stage :work :message "boom"}})))))) (t/is (= [] (get-progresss job-id))))) @@ -243,7 +248,7 @@ (jobs/claim cfg job-id1 (:scheduled-at (th/db-get :job {:id job-id1} :id :scheduled-at))) (jobs/claim cfg job-id2 (:scheduled-at (th/db-get :job {:id job-id2} :id :scheduled-at))) (jobs/heartbeat cfg :job-id job-id1) - (jobs/heartbeat cfg :job-id job-id2 :progress {:current 1}) + (jobs/heartbeat cfg :job-id job-id2 :progress {:stage :work}) (jobs/complete cfg :job-id job-id1) (jobs/fail cfg job-id2 test-error) (t/is (not (contains? @@#'jobs/heartbeats job-id1))) @@ -300,7 +305,7 @@ (t/is (= {:action :skip} (:result out))))) (t/testing "progress on a terminal row reports skip" (let [out (mgmt :report-job-progress {:job-id done-id - :progress {:total 10 :current 4}})] + :progress {:stage :pages :counters {:pages {:current 4 :total 10}}}})] (t/is (nil? (:error out))) (t/is (= {:action :skip} (:result out)))))) (let [cfg (make-cfg) @@ -309,7 +314,7 @@ (t/testing "live paths report run" (t/is (= {:action :run} (:result (mgmt :report-job-progress {:job-id job-id - :progress {:total 10 :current 4}})))) + :progress {:stage :pages :counters {:pages {:current 4 :total 10}}}})))) (t/is (= {:action :run} (:result (mgmt :complete-job {:job-id job-id :result {:x 1}}))))))) diff --git a/backend/test/backend_tests/rpc_quotes_test.clj b/backend/test/backend_tests/rpc_quotes_test.clj index c691ea678b..84493e1ed4 100644 --- a/backend/test/backend_tests/rpc_quotes_test.clj +++ b/backend/test/backend_tests/rpc_quotes_test.clj @@ -6,9 +6,12 @@ (ns backend-tests.rpc-quotes-test (:require + [app.common.time :as ct] [app.common.uuid :as uuid] + [app.config :as cf] [app.db :as db] [app.http :as http] + [app.jobs :as jobs] [app.rpc :as-alias rpc] [app.rpc.cond :as cond] [app.rpc.quotes :as-alias quotes] @@ -472,3 +475,78 @@ (t/is (= :restriction (th/ex-type error))) (t/is (= :max-quote-reached (th/ex-code error))) (t/is (= "media-storage-bytes-per-team" (:target (ex-data error)))))))) + +(t/deftest binfile-jobs-per-profile-quote + (with-mocks [mock {:target 'app.config/get + :return (th/config-get-mock + {:quotes-export-jobs-per-profile 2 + :quotes-import-jobs-per-profile 1})}] + + (let [profile (th/create-profile* 1) + other (th/create-profile* 2) + + insert! (fn [profile-id name status & {:keys [tenant]}] + (th/db-insert! :job {:id (uuid/next) + :name name + :tenant (or tenant (cf/get :tenant)) + :queue "binfile" + :params (db/json {}) + :priority 100 + :max-retries 0 + :retry-num 0 + :status status + :profile-id profile-id + :scheduled-at (ct/now) + :created-at (ct/now) + :modified-at (ct/now)})) + + check! (fn check! + ([id profile-id] (check! th/*system* id profile-id)) + ([cfg id profile-id] + (quotes/check! cfg {::quotes/id id + ::quotes/profile-id profile-id}))) + + error (fn [& args] + (try (apply check! args) nil (catch Throwable cause cause)))] + + (t/testing "a profile without jobs is under the quote" + (t/is (nil? (check! ::quotes/export-jobs-per-profile (:id profile))))) + + (t/testing "the jobs of another profile do not count" + (insert! (:id other) "export-binfile" "new") + (insert! (:id profile) "export-binfile" "new") + (t/is (nil? (check! ::quotes/export-jobs-per-profile (:id profile))))) + + (t/testing "the jobs of another tenant do not count either" + (insert! (:id profile) "export-binfile" "new" :tenant "another-tenant") + (t/is (nil? (check! ::quotes/export-jobs-per-profile (:id profile))))) + + (t/testing "a job that already reached a terminal state does not count" + (insert! (:id profile) "export-binfile" "completed") + (insert! (:id profile) "export-binfile" "failed") + (insert! (:id profile) "export-binfile" "cancelled") + (t/is (nil? (check! ::quotes/export-jobs-per-profile (:id profile))))) + + (t/testing "the job that would go over the limit is rejected" + (insert! (:id profile) "export-binfile" "running") + (let [cause (error ::quotes/export-jobs-per-profile (:id profile))] + (t/is (some? cause)) + (t/is (= :restriction (:type (ex-data cause)))) + (t/is (= :max-quote-reached (:code (ex-data cause)))) + (t/is (= "export-jobs-per-profile" (:target (ex-data cause)))))) + + (t/testing "the families are independent" + (t/is (nil? (check! ::quotes/import-jobs-per-profile (:id profile)))) + (insert! (:id profile) "import-binfile" "new") + (t/is (some? (error ::quotes/import-jobs-per-profile (:id profile))))) + + (t/testing "a new name of the family is covered by the registry" + (let [future (th/create-profile* 3) + defs (assoc (::jobs/defs th/*system*) + :export-assets {::jobs/name :export-assets + ::jobs/family :export}) + cfg (assoc th/*system* ::jobs/defs defs)] + (insert! (:id future) "export-assets" "new") + (t/is (nil? (check! cfg ::quotes/export-jobs-per-profile (:id future)))) + (insert! (:id future) "export-assets" "new") + (t/is (some? (error cfg ::quotes/export-jobs-per-profile (:id future))))))))) diff --git a/backend/test/backend_tests/storage_test.clj b/backend/test/backend_tests/storage_test.clj index a49d5b4717..930e059433 100644 --- a/backend/test/backend_tests/storage_test.clj +++ b/backend/test/backend_tests/storage_test.clj @@ -113,6 +113,44 @@ :content-type "text/plain"})] (t/is (not= (:id object1) (:id object2))))) +(t/deftest job-resource-objects-require-an-owner + (let [storage (-> (:app.storage/storage th/*system*) + (configure-storage-backend)) + content (sto/content "content")] + + (t/testing "an object of this bucket cannot be stored without its owner" + (t/is (thrown? Throwable + (sto/put-object! storage {::sto/content content + :bucket sto/job-resource-bucket + :content-type "text/plain"})))) + + (t/testing "and storing it with an owner still works" + (t/is (some? (sto/put-object! storage {::sto/content content + :bucket sto/job-resource-bucket + :profile-id (uuid/next) + :content-type "text/plain"})))))) + +(t/deftest job-resource-objects-are-not-deduplicated + ;; An object in this bucket belongs to one profile: sharing a blob + ;; between two owners would serve one user's artifact to another. + (let [storage (-> (:app.storage/storage th/*system*) + (configure-storage-backend)) + content (-> (sto/content "content") + (sto/wrap-with-hash "same-hash")) + object1 (sto/put-object! storage {::sto/content content + ::sto/deduplicate? true + ::sto/touched-at (ct/now) + :bucket sto/job-resource-bucket + :profile-id (uuid/next) + :content-type "text/plain"}) + object2 (sto/put-object! storage {::sto/content content + ::sto/deduplicate? true + ::sto/touched-at (ct/now) + :bucket sto/job-resource-bucket + :profile-id (uuid/next) + :content-type "text/plain"})] + (t/is (not= (:id object1) (:id object2))))) + (t/deftest put-and-retrieve-expired-object (let [storage (-> (:app.storage/storage th/*system*) (configure-storage-backend)) @@ -1271,6 +1309,7 @@ job-id (uuid/next) object (sto/put-object! storage {::sto/content (sto/content "content") :content-type "text/plain" + :profile-id (uuid/next) :bucket sto/job-resource-bucket})] ;; the object is created with the job-resource bucket in metadata @@ -1319,6 +1358,7 @@ ;; a touched object with no referencing job row at all is deleted too (let [object (sto/put-object! storage {::sto/content (sto/content "content") :content-type "text/plain" + :profile-id (uuid/next) :bucket sto/job-resource-bucket})] (th/db-update! :storage-object {:touched-at (ct/now)} {:id (:id object)}) (let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:hours 3}))] diff --git a/backend/test/backend_tests/tasks/binfile_jobs_integration_test.clj b/backend/test/backend_tests/tasks/binfile_jobs_integration_test.clj new file mode 100644 index 0000000000..54a6372163 --- /dev/null +++ b/backend/test/backend_tests/tasks/binfile_jobs_integration_test.clj @@ -0,0 +1,268 @@ +;; 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 SUBSIDIARY SL + +(ns backend-tests.tasks.binfile-jobs-integration-test + "The whole chain of a binfile job, without a full end-to-end: the + command creates the job, the dispatcher sends it to its own queue, the + runner of that queue executes it and the artifact it produced (or + consumed) ends up where the ledger says." + (:require + [app.binfile.common :as bfc] + [app.binfile.jobs :as bfj] + [app.binfile.v3 :as v3] + [app.common.time :as ct] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.jobs :as jobs] + [app.metrics :as-alias mtx] + [app.msgbus :as-alias mbus] + [app.redis :as rds] + [app.rpc :as-alias rpc] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [app.worker :as wrk] + [app.worker.dispatcher :as wdisp] + [app.worker.runner :as wrkr] + [backend-tests.helpers :as th] + [clojure.java.io :as jio] + [clojure.test :as t] + [datoteka.io :as io])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each (th/serial th/database-reset th/clean-storage)) + +(def ^:private fixture + "A real penpot export, the same fixture the binfile tests read." + "backend_tests/test_files/svg-attrs-camel-case.penpot") + +(def ^:private fast-timeout + "A runner of an empty queue must not make the test wait for the default + timeout of the production loop." + (ct/duration {:millis 200})) + +(defn- get-storage + [] + (assoc (:app.storage/storage th/*system*) ::sto/backend :fs)) + +(defn- import-fixture! + "A file in the default project of the profile, imported from the + fixture through the core." + [profile] + (-> th/*system* + (assoc ::bfc/project-id (:default-project-id profile)) + (assoc ::bfc/profile-id (:id profile)) + (assoc ::bfc/team-id (:default-team-id profile)) + (assoc ::bfc/input (-> fixture io/resource jio/file)) + (v3/import-files!))) + +(defn- upload-chunked! + "Upload the fixture as a single chunk and return the session id." + [profile] + (let [source (-> fixture io/resource jio/file) + session-id (-> (th/command! {::th/type :create-upload-session + ::rpc/profile-id (:id profile) + :total-chunks 1}) + :result :session-id) + out (th/command! {::th/type :upload-chunk + ::rpc/profile-id (:id profile) + :session-id session-id + :index 0 + :content {:filename "package.penpot" + :path (th/tempfile fixture) + :mtype "application/zip" + :size (.length source)}})] + (t/is (nil? (:error out))) + session-id)) + +(defn- dispatcher-cfg + [] + {::db/pool th/*pool* + ::rds/client (get th/*system* :app.redis/client) + ::mtx/metrics (get th/*system* :app.metrics/metrics) + ::wrk/tenant (cf/get :tenant) + ::wdisp/lease (cf/get-jobs-lease) + ::wdisp/timeout (ct/duration "10s")}) + +(defn- runner-cfg + "The cfg the runner loop needs, with the real job-defs of the system: + the handler closes over the component of its own job-def." + [queue] + {::db/pool th/*pool* + ::rds/conn (rds/connect {::rds/client (get th/*system* :app.redis/client) + ::mtx/metrics (get th/*system* :app.metrics/metrics)}) + ::jobs/defs (::jobs/defs th/*system*) + ::mtx/metrics (get th/*system* :app.metrics/metrics) + ::mbus/msgbus (get th/*system* :app.msgbus/msgbus) + ::wrkr/id "test-runner" + ::wrkr/queue queue + ::wrk/tenant (cf/get :tenant) + ::wrkr/timeout fast-timeout}) + +(defn- run-one + [cfg] + (@#'wrkr/run-worker-loop cfg)) + +(defn- job-row + [job-id] + (-> (th/db-get :job {:id job-id}) + (update :result #(cond-> % (db/pgobject? %) db/decode-json-pgobject)) + (update :error jobs/decode-job-error))) + +(defn- project-files + [project-id] + (th/db-exec! ["SELECT id FROM file WHERE project_id = ? AND deleted_at IS NULL" + project-id])) + +(defn- download + "The artifact as a local file, as a client would fetch it." + [resource-id] + (let [object (sto/get-object (get-storage) resource-id) + path (tmp/tempfile* :suffix ".penpot")] + (with-open [out (io/output-stream path)] + (io/copy (sto/get-object-data (get-storage) object) out)) + path)) + +(defn- create-export-binfile-job + [profile file-ids] + (th/command! {::th/type :create-export-binfile-job + ::rpc/profile-id (:id profile) + :params {:file-ids file-ids + :export-type :detach-libraries}})) + +(defn- create-import-binfile-job + [profile project-id] + (th/command! {::th/type :create-import-binfile-job + ::rpc/profile-id (:id profile) + :params {:project-id project-id + :name "imported"} + :upload-id (upload-chunked! profile)})) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TESTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest the-export-job-runs-to-completion-in-its-own-queue + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + out (create-export-binfile-job profile #{file-id}) + job-id (:id (:result out))] + + (t/is (th/success? out)) + + (wdisp/run-batch (dispatcher-cfg)) + (t/testing "the dispatcher schedules it, and it waits for its queue" + (t/is (= "scheduled" (:status (jobs/get-job th/*system* job-id))))) + + (t/testing "a runner of another queue does not take it" + (run-one (runner-cfg "default")) + (t/is (= "scheduled" (:status (jobs/get-job th/*system* job-id))))) + + (t/testing "the runner of its queue does" + (run-one (runner-cfg "binfile")) + (t/is (= "completed" (:status (jobs/get-job th/*system* job-id))))) + + (let [row (job-row job-id) + job (:result row)] + (t/testing "the runner associated the artifact of the completion envelope" + (t/is (some? (:resource-id row)))) + + (t/testing "the result of the job is the descriptor the handler built" + (t/is (= "export.penpot" (:filename job))) + (t/is (= "application/zip" (:mtype job))) + (t/is (pos? (:size job))) + (t/is (some? (:resource-uri job)))) + + (t/testing "and the artifact is the package of those files" + (t/is (= [file-id] + (mapv :id (:files (v3/get-manifest th/*system* + (download (:resource-id row)))))))) + + (t/testing "the artifact belongs to the profile of the job" + (let [object (sto/get-object (get-storage) (:resource-id row))] + (t/is (= sto/job-resource-bucket (:bucket (meta object)))) + (t/is (= (:id profile) (:profile-id (meta object)))))) + + (t/testing "the runner reached the terminal state through the row" + (t/is (= "completed" (:status row))) + (t/is (some? (:completed-at row))))))) + +(t/deftest the-import-job-runs-to-completion-in-its-own-queue + (let [profile (th/create-profile* 1) + project-id (:default-project-id profile) + out (create-import-binfile-job profile project-id) + job-id (:id (:result out)) + ;; read before the run: the handler drops the reference as soon as + ;; it releases the package + input-id (:resource-id (th/db-get :job {:id job-id}))] + + (t/is (th/success? out)) + + (wdisp/run-batch (dispatcher-cfg)) + (run-one (runner-cfg "binfile")) + + (let [row (job-row job-id) + job (:result row)] + (t/testing "the job ends completed" + (t/is (= "completed" (:status row)))) + + (t/testing "the files of the package are in the destination" + ;; the result is JSON: its ids come back as text, the rows as uuids + (t/is (= (set (map str (:file-ids job))) + (set (map (comp str :id) (project-files project-id)))))) + + (t/testing "the result carries the files and their resolution" + (t/is (pos? (count (:file-ids job)))) + (t/is (= "imported" (:name job)))) + + (t/testing "the package it consumed is released" + (t/is (some? (:deleted-at (th/db-get :storage-object + {:id input-id} + {::db/remove-deleted false}))))) + + (t/testing "and the job no longer points at it" + (t/is (nil? (:resource-id row))))))) + +(t/deftest the-adapter-reports-progress-through-heartbeat + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (uuid/next) + calls (atom [])] + + (th/db-insert! :job {:id job-id + :name "export-binfile" + :tenant (cf/get :tenant) + :queue "binfile" + :params (db/json {}) + :priority 100 + :max-retries 0 + :retry-num 0 + :status "running" + :profile-id (:id profile) + :scheduled-at (ct/now) + :created-at (ct/now) + :modified-at (ct/now)}) + + (with-redefs [jobs/heartbeat (fn [_cfg & {:as options}] + (swap! calls conj options) + 1)] + (bfj/export-files th/*system* + (jobs/make-context (jobs/get-job th/*system* job-id)) + {:ids #{file-id} + :export-type :detach-libraries + :output (tmp/tempfile* :suffix ".penpot")})) + + (let [reports (map :progress @calls)] + + (t/testing "the adapter reports progress through heartbeat" + (t/is (pos? (count reports)))) + + (t/testing "with the payload of the job contract, and nothing else" + (t/is (every? keyword? (map :stage reports))) + (t/is (every? #(<= (count (keys %)) 2) reports))) + + (t/testing "about the job of the context, not one the caller named" + (t/is (every? #(= job-id (:job-id %)) @calls)))))) diff --git a/backend/test/backend_tests/tasks/export_binfile_test.clj b/backend/test/backend_tests/tasks/export_binfile_test.clj new file mode 100644 index 0000000000..80fcdedd36 --- /dev/null +++ b/backend/test/backend_tests/tasks/export_binfile_test.clj @@ -0,0 +1,193 @@ +;; 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 SUBSIDIARY SL + +(ns backend-tests.tasks.export-binfile-test + "The `:export-binfile` job: what it produces, who may ask for it and + what it leaves behind when it fails." + (:require + [app.binfile.common :as bfc] + [app.binfile.v3 :as v3] + [app.common.exceptions :as ex] + [app.common.time :as ct] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.jobs :as jobs] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [backend-tests.helpers :as th] + [clojure.java.io :as jio] + [clojure.test :as t] + [cuerdas.core :as str] + [datoteka.io :as io])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each (th/serial th/database-reset th/clean-storage)) + +(def ^:private fixture + "A real penpot export, the same fixture the binfile tests read." + "backend_tests/test_files/svg-attrs-camel-case.penpot") + +(defn- get-storage + [] + (assoc (:app.storage/storage th/*system*) ::sto/backend :fs)) + +(defn- import-fixture! + "A file in the default project of the profile, imported from the + fixture through the core." + [profile] + (-> th/*system* + (assoc ::bfc/project-id (:default-project-id profile)) + (assoc ::bfc/profile-id (:id profile)) + (assoc ::bfc/team-id (:default-team-id profile)) + (assoc ::bfc/input (-> fixture io/resource jio/file)) + (v3/import-files!))) + +(defn- make-job! + "A job row as the substrate writes it: running and owned by a profile." + [profile-id] + (let [id (uuid/next)] + (th/db-insert! :job {:id id + :name "export-binfile" + :tenant (cf/get :tenant) + :queue "binfile" + :params (db/json {}) + :priority 100 + :max-retries 0 + :retry-num 0 + :status "running" + :profile-id profile-id + :scheduled-at (ct/now) + :created-at (ct/now) + :modified-at (ct/now)}) + id)) + +(defn- run-export + "Run the export handler of a job, as the runner does." + [job-id params] + (jobs/invoke (-> th/*system* + (assoc ::jobs/name :export-binfile) + (assoc ::jobs/params params) + (assoc ::jobs/context + (jobs/make-context (jobs/get-job th/*system* job-id)))))) + +(defn- count-storage-objects + [] + (count (th/db-exec! ["SELECT id FROM storage_object"]))) + +(defn- temp-files + "The temporary files the export leaves in the tmp directory." + [] + (seq (.list (jio/file tmp/default-tmp-dir)))) + +(defn- download + "The artifact as a local file, as a client would fetch it." + [resource-id] + (let [object (sto/get-object (get-storage) resource-id) + path (tmp/tempfile* :suffix ".penpot")] + (with-open [out (io/output-stream path)] + (io/copy (sto/get-object-data (get-storage) object) out)) + path)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TESTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest the-export-handler-stores-the-artifact-of-its-job + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (make-job! (:id profile)) + result (run-export job-id {:file-ids #{file-id} + :export-type :detach-libraries}) + object (sto/get-object (get-storage) (:resource-id result))] + + (t/testing "the handler answers with the completion envelope" + (t/is (uuid? (:resource-id result))) + (t/is (= "export.penpot" (get-in result [:result :filename]))) + (t/is (= "application/zip" (get-in result [:result :mtype]))) + (t/is (pos? (get-in result [:result :size]))) + (t/is (str/includes? (get-in result [:result :resource-uri]) + (str (:resource-id result))))) + + (t/testing "the artifact is a job resource owned by the profile of the job" + (t/is (= sto/job-resource-bucket (:bucket (meta object)))) + (t/is (= (:id profile) (:profile-id (meta object))))) + + (t/testing "it is touched, so a crash before the completion leaves it reclaimable" + (t/is (some? (:touched-at (th/db-get :storage-object + {:id (:resource-id result)}))))) + + (t/testing "the package is the one the core produces for those files" + (let [manifest (v3/get-manifest th/*system* (download (:resource-id result)))] + (t/is (= [file-id] (mapv :id (:files manifest)))))) + + (t/testing "the completion of the job associates the artifact" + (jobs/complete th/*system* + :job-id job-id + :result (:result result) + :resource-id (:resource-id result)) + (t/is (= (:resource-id result) + (:resource-id (jobs/get-job th/*system* job-id))))))) + +(t/deftest the-export-handler-revalidates-the-read-permission + (let [owner (th/create-profile* 1) + other (th/create-profile* 2) + file-id (first (:file-ids (import-fixture! owner))) + job-id (make-job! (:id other)) + before (count-storage-objects)] + + (t/testing "a job whose owner cannot read a file does not export it" + (t/is (thrown? Throwable + (run-export job-id {:file-ids #{file-id} + :export-type :detach-libraries})))) + + (t/testing "and nothing was stored for it" + (t/is (= before (count-storage-objects))) + (t/is (nil? (:resource-id (jobs/get-job th/*system* job-id))))) + + (t/testing "the temporary file is not left behind either" + (t/is (empty? (temp-files)))))) + +(t/deftest the-export-handler-leaves-nothing-when-the-storage-fails + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (make-job! (:id profile)) + before (count-storage-objects)] + + (with-redefs [sto/put-object! (fn [& _] + (ex/raise :type :internal + :code :boom + :hint "storage is down"))] + (t/is (thrown? Throwable + (run-export job-id {:file-ids #{file-id} + :export-type :detach-libraries})))) + + (t/testing "the job never comes to own an artifact" + (t/is (nil? (:resource-id (jobs/get-job th/*system* job-id))))) + + (t/testing "no artifact was stored" + (t/is (= before (count-storage-objects)))) + + (t/testing "and the temporary file of the export is gone" + (t/is (empty? (temp-files)))))) + +(t/deftest the-export-handler-leaves-nothing-behind-when-the-job-is-cancelled + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + job-id (make-job! (:id profile)) + before (count-storage-objects)] + + (th/db-update! :job {:status "cancelled"} {:id job-id}) + + (t/is (thrown? Throwable + (run-export job-id {:file-ids #{file-id} + :export-type :detach-libraries}))) + + (t/testing "no artifact was stored" + (t/is (= before (count-storage-objects)))) + + (t/testing "and the temporary file of the export was cleaned up" + (t/is (empty? (temp-files)))))) diff --git a/backend/test/backend_tests/tasks/import_binfile_test.clj b/backend/test/backend_tests/tasks/import_binfile_test.clj new file mode 100644 index 0000000000..e2c619177f --- /dev/null +++ b/backend/test/backend_tests/tasks/import_binfile_test.clj @@ -0,0 +1,254 @@ +;; 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 SUBSIDIARY SL + +(ns backend-tests.tasks.import-binfile-test + "The `:import-binfile` job: what it imports, who may ask for it and what + it leaves behind when it fails." + (:require + [app.binfile.common :as bfc] + [app.binfile.v1 :as v1] + [app.binfile.v3 :as v3] + [app.common.time :as ct] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.jobs :as jobs] + [app.jobs.storage :as js] + [app.storage :as sto] + [app.storage.tmp :as tmp] + [backend-tests.helpers :as th] + [clojure.java.io :as jio] + [clojure.test :as t] + [datoteka.io :as io])) + +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each (th/serial th/database-reset th/clean-storage)) + +(def ^:private fixture + "A real penpot export, the same fixture the binfile tests read." + "backend_tests/test_files/svg-attrs-camel-case.penpot") + +(defn- fixture-path + [] + (-> fixture io/resource jio/file)) + +(defn- import-fixture! + "A file in the default project of the profile, imported from the + fixture through the core." + [profile] + (-> th/*system* + (assoc ::bfc/project-id (:default-project-id profile)) + (assoc ::bfc/profile-id (:id profile)) + (assoc ::bfc/team-id (:default-team-id profile)) + (assoc ::bfc/input (fixture-path)) + (v3/import-files!))) + +(defn- export-to-v1! + "A version 1 package of a file, the format the old exporter writes." + [file-id] + (let [output (tmp/tempfile* :suffix ".penpot")] + (v1/export-files! (-> th/*system* + (assoc ::bfc/ids #{file-id}) + (assoc ::bfc/include-libraries false) + (assoc ::bfc/embed-assets false)) + output) + output)) + +(defn- stage-package! + "The package a job consumes, as its resource." + [profile path] + (js/put-resource th/*system* (:id profile) + {:content (sto/content path) + :filename "package.penpot" + :mtype "application/zip"})) + +(defn- make-job! + "A job row as the substrate writes it: running, owned by a profile and + holding the package it consumes." + [profile-id resource-id] + (let [id (uuid/next)] + (th/db-insert! :job {:id id + :name "import-binfile" + :tenant (cf/get :tenant) + :queue "binfile" + :params (db/json {}) + :priority 100 + :max-retries 0 + :retry-num 0 + :status "running" + :profile-id profile-id + :resource-id resource-id + :scheduled-at (ct/now) + :created-at (ct/now) + :modified-at (ct/now)}) + id)) + +(defn- run-import + "Run the import handler of a job, as the runner does." + [job-id params] + (jobs/invoke (-> th/*system* + (assoc ::jobs/name :import-binfile) + (assoc ::jobs/params params) + (assoc ::jobs/context + (jobs/make-context (jobs/get-job th/*system* job-id)))))) + +(defn- project-files + [project-id] + (th/db-exec! ["SELECT id FROM file WHERE project_id = ? AND deleted_at IS NULL" + project-id])) + +(defn- input-row + [resource-id] + (th/db-get :storage-object {:id resource-id} {::db/remove-deleted false})) + +(defn- temp-files + [] + (seq (.list (jio/file tmp/default-tmp-dir)))) + +(defn- project-modified-at + [project-id] + (:modified-at (th/db-get :project {:id project-id}))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TESTS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest the-import-handler-imports-the-package-of-its-job + (let [profile (th/create-profile* 1) + staged (stage-package! profile (fixture-path)) + job-id (make-job! (:id profile) (:resource-id staged)) + result (run-import job-id {:project-id (:default-project-id profile) + :name "imported" + :version 3})] + + (t/testing "the handler answers with the files it created" + (t/is (= 1 (count (:file-ids result)))) + (t/is (= "imported" (:name result))) + (t/is (= 3 (:version result))) + (t/testing "and the file is in the destination project" + (t/is (= (set (:file-ids result)) + (set (map :id (project-files (:default-project-id profile)))))))) + + (t/testing "the package of the job is released once the job is terminal" + (t/is (some? (:deleted-at (input-row (:resource-id staged)))))) + + (t/testing "and its temporary copy is gone" + (t/is (empty? (temp-files)))))) + +(t/deftest the-import-marks-the-destination-project-as-modified + (let [profile (th/create-profile* 1) + project-id (:default-project-id profile) + staged (stage-package! profile (fixture-path)) + job-id (make-job! (:id profile) (:resource-id staged)) + before (project-modified-at project-id)] + + (run-import job-id {:project-id project-id + :name "imported" + :version 3}) + + (t/testing "the project the import wrote into is not left looking untouched" + (t/is (pos? (- (inst-ms (project-modified-at project-id)) + (inst-ms before))))))) + +(t/deftest the-import-handler-imports-a-version-1-package + (let [profile (th/create-profile* 1) + file-id (first (:file-ids (import-fixture! profile))) + staged (stage-package! profile (export-to-v1! file-id)) + job-id (make-job! (:id profile) (:resource-id staged)) + result (run-import job-id {:project-id (:default-project-id profile) + :name "imported-v1" + :version 1})] + + (t/is (= 1 (count (:file-ids result)))) + (t/is (= 1 (:version result))))) + +(t/deftest the-import-handler-revalidates-the-edit-permission + (let [owner (th/create-profile* 1) + other (th/create-profile* 2) + staged (stage-package! other (fixture-path)) + ;; the package belongs to `other`, but the destination project + ;; belongs to `owner`: an import there is not allowed + job-id (make-job! (:id other) (:resource-id staged)) + before (project-files (:default-project-id owner))] + + (t/testing "a job that cannot edit the destination does not import it" + (t/is (thrown? Throwable + (run-import job-id {:project-id (:default-project-id owner) + :name "imported" + :version 3})))) + + (t/testing "nothing was written in the destination" + (t/is (= before (project-files (:default-project-id owner))))) + + (t/testing "the package is released anyway: the job is terminal" + (t/is (some? (:deleted-at (input-row (:resource-id staged)))))))) + +(t/deftest a-job-that-was-already-cancelled-never-imports + (let [profile (th/create-profile* 1) + staged (stage-package! profile (fixture-path)) + job-id (make-job! (:id profile) (:resource-id staged)) + before (project-files (:default-project-id profile))] + + (th/db-update! :job {:status "cancelled"} {:id job-id}) + + (t/testing "a cancelled job does not import anything" + (t/is (thrown? Throwable + (run-import job-id {:project-id (:default-project-id profile) + :name "imported" + :version 3}))) + (t/is (= before (project-files (:default-project-id profile))))) + + (t/testing "and the package is released" + (t/is (some? (:deleted-at (input-row (:resource-id staged)))))))) + +(t/deftest an-import-cancelled-while-it-runs-leaves-nothing-behind + (let [profile (th/create-profile* 1) + staged (stage-package! profile (fixture-path)) + job-id (make-job! (:id profile) (:resource-id staged)) + before (project-files (:default-project-id profile))] + + ;; the user cancels while the import is running: the first progress + ;; report is a good moment for it, because the core is already + ;; writing by then + (with-redefs [jobs/heartbeat (fn [& _] + (th/db-update! :job {:status "cancelled"} {:id job-id}) + 1)] + (t/is (thrown? Throwable + (run-import job-id {:project-id (:default-project-id profile) + :name "imported" + :version 3})))) + + (t/testing "the import is rolled back: the destination keeps no files" + (t/is (= before (project-files (:default-project-id profile))))) + + (t/testing "and the package is released anyway" + (t/is (some? (:deleted-at (input-row (:resource-id staged)))))))) + +(t/deftest a-package-that-is-not-a-penpot-file-fails-without-writing + (let [profile (th/create-profile* 1) + staged (stage-package! profile (fixture-path)) + job-id (make-job! (:id profile) (:resource-id staged)) + before (project-files (:default-project-id profile))] + + ;; the bytes of the resource are replaced by something that is not a + ;; package at all: the core must fail instead of importing garbage + (sto/del-object! (:app.storage/storage th/*system*) (:resource-id staged)) + (let [broken (js/put-resource th/*system* (:id profile) + {:content (sto/content "not a penpot package") + :filename "package.penpot" + :mtype "application/zip"})] + (th/db-update! :job {:resource-id (:resource-id broken)} {:id job-id}) + + (t/is (thrown? Throwable + (run-import job-id {:project-id (:default-project-id profile) + :name "imported" + :version 3}))) + + (t/testing "nothing was imported" + (t/is (= before (project-files (:default-project-id profile))))) + + (t/testing "and the broken package is released too" + (t/is (some? (:deleted-at (input-row (:resource-id broken))))))))) diff --git a/backend/test/backend_tests/util_events_test.clj b/backend/test/backend_tests/util_events_test.clj new file mode 100644 index 0000000000..9c75d32699 --- /dev/null +++ b/backend/test/backend_tests/util_events_test.clj @@ -0,0 +1,77 @@ +;; 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 SUBSIDIARY SL + +(ns backend-tests.util-events-test + "The events subsystem: a tap delivers to whatever sink is bound, a + function inline on the same thread or a channel for a listener of its + own, and untracked runs pay nothing." + (:require + [app.util.events :as events] + [clojure.test :as t] + [promesa.exec :as px] + [promesa.exec.csp :as sp])) + +(t/deftest tap-outside-a-run-is-a-no-op + (t/testing "a tap with no sink bound does nothing" + (t/is (nil? (events/tap :one 1))))) + +(t/deftest tap-calls-a-function-sink-inline + (let [seen (atom [])] + (binding [events/*sink* (fn [event] (swap! seen conj event))] + (t/testing "every tap reaches the function, in order" + (t/is (nil? (events/tap :one 1))) + (t/is (nil? (events/tap :two 2))) + (t/is (= [[:one 1] [:two 2]] @seen)))))) + +(t/deftest tap-puts-on-a-channel-sink + (let [channel (sp/chan :buf 4)] + (try + (binding [events/*sink* channel] + (t/testing "a tap puts the event on the channel" + (t/is (nil? (events/tap :one 1))) + (t/is (= [:one 1] (sp/take! channel))))) + (finally + (sp/close! channel))))) + +(t/deftest an-exception-from-a-function-sink-propagates + (let [cause (ex-info "the job is no longer active" + {:type :interrupt :code :job-interrupted})] + (binding [events/*sink* (fn [_event] (throw cause))] + (t/testing "the tap raises the very exception the sink threw" + (t/is (identical? cause + (try (events/tap :one 1) nil + (catch Throwable raised raised)))))))) + +(t/deftest spawn-listener-consumes-until-the-channel-closes + (let [seen (atom []) + ended (promise) + channel (sp/chan :buf 4) + listener (events/spawn-listener + channel + (fn [event] (swap! seen conj event)) + (fn [] (deliver ended true)))] + (sp/put! channel [:one 1]) + (sp/put! channel [:two 2]) + (sp/close! channel) + (px/await! listener) + (t/testing "the listener saw every event, in order" + (t/is (= [[:one 1] [:two 2]] @seen))) + (t/testing "and the close hook ran" + (t/is (true? (deref ended 1000 false)))))) + +(t/deftest spawn-listener-stops-on-a-consumer-failure + (let [seen (atom []) + channel (sp/chan :buf 4) + listener (events/spawn-listener + channel + (fn [_event] (throw (ex-info "boom" {}))) + (fn [] nil))] + (sp/put! channel [:one 1]) + (sp/put! channel [:two 2]) + (px/await! listener) + (t/testing "the failure stops the listener, so nothing is consumed" + (t/is (= [] @seen))) + (sp/close! channel))) diff --git a/backend/test/backend_tests/worker_runner_test.clj b/backend/test/backend_tests/worker_runner_test.clj index 415d444ab8..2df99415a3 100644 --- a/backend/test/backend_tests/worker_runner_test.clj +++ b/backend/test/backend_tests/worker_runner_test.clj @@ -221,14 +221,15 @@ (push-payload job-id scheduled-at) (run-one (mk-cfg {})) - (t/testing "the handler receives exactly the four context keys" + (t/testing "the handler receives exactly the five context keys" (t/is (= 1 (count @received-contexts))) (let [context (first @received-contexts)] - (t/is (= #{:id :name :label :resource-id} (set (keys context)))) + (t/is (= #{:id :name :label :resource-id :profile-id} (set (keys context)))) (t/is (= job-id (:id context))) (t/is (= "echo-runner" (:name context))) (t/is (= "runner-label" (:label context))) (t/is (nil? (:resource-id context))) + (t/is (nil? (:profile-id context)) "an internal job has no owner") (t/testing "and nothing of the row leaks into it" (t/is (not (contains? context :params))) diff --git a/backend/test/backend_tests/worker_test.clj b/backend/test/backend_tests/worker_test.clj index 75f87be0c4..02f525820d 100644 --- a/backend/test/backend_tests/worker_test.clj +++ b/backend/test/backend_tests/worker_test.clj @@ -7,6 +7,11 @@ (ns backend-tests.worker-test (:require [app.common.schema :as sm] + [app.config :as cf] + [app.db :as db] + [app.jobs :as jobs] + [app.main :as main] + [app.metrics :as-alias mtx] [app.msgbus :as mbus] [app.worker :as wrk] [app.worker.cron :as wcron] @@ -44,3 +49,20 @@ @#'wdisp/schema:dispatcher @#'wrkr/schema:params]] (t/is (sm/schema? (sm/schema s)))))) + +(t/deftest the-binfile-runner-is-wired + (let [cfg (get main/worker-config [:app.main/binfile ::wrk/runner])] + + (t/is (some? cfg)) + + (t/testing "the heavy work of a user job does not share the default runner" + (t/is (= :binfile (::wrk/queue cfg))) + (t/is (= (cf/get :worker-binfile-parallelism 1) (::wrk/parallelism cfg)))) + + (t/testing "a job of a profile cannot store its events without a msgbus" + (t/is (contains? cfg ::mbus/msgbus))) + + (t/testing "and the runner resolves jobs, metrics and the pool" + (t/is (contains? cfg ::jobs/defs)) + (t/is (contains? cfg ::mtx/metrics)) + (t/is (contains? cfg ::db/pool))))) diff --git a/frontend/src/app/main/data/exports/files.cljs b/frontend/src/app/main/data/exports/files.cljs index d50948e26e..5758f38ee4 100644 --- a/frontend/src/app/main/data/exports/files.cljs +++ b/frontend/src/app/main/data/exports/files.cljs @@ -10,9 +10,10 @@ [app.common.data :as d] [app.common.schema :as sm] [app.main.data.event :as ev] + [app.main.data.jobs :as dj] [app.main.data.modal :as modal] [app.main.repo :as rp] - [app.util.sse :as sse] + [app.main.store :as st] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -59,28 +60,73 @@ :team-id team-id :files files})))))))))) +(defn- export-file + "One file, one job: the file is exported on its own, so a failure does + not touch the others, and the artifact is read from the result of the + job once it is over. + + Every step of the job becomes a message for the caller: the milestone + under `:progress`, the artifact as `:uri` when the job completed, and + the public error of the job when it failed. The queue of a file is + announced by `export-files` before any job exists and only ends with a + milestone: a worker picking the job up is not one. + + `on-job` is an optional callback invoked with `{:job-id ... :file-id + ...}` for the created job, so the caller can track it per file and + cancel it while it runs." + [ws-conn type file on-job] + (->> (rp/cmd! :create-export-binfile-job + {:params {:file-ids #{(:id file)} + :export-type type}}) + (rx/mapcat (fn [{job-id :id}] + (when (fn? on-job) + (on-job {:job-id job-id :file-id (:id file)})) + (->> (dj/watch-job ws-conn job-id) + (rx/mapcat (fn [{:keys [kind status result error] :as emission}] + (cond + (= "completed" status) + (rx/of {:file-id (:id file) + :uri (:resource-uri result) + :filename (:name file)}) + + (= "failed" status) + (rx/of {:file-id (:id file) + :error error}) + + (= "cancelled" status) + (rx/of {:file-id (:id file) + :cancelled true}) + + (= :progress kind) + (rx/of {:file-id (:id file) + :progress (:payload emission)}) + + :else + (rx/empty))))))) + (rx/catch (fn [cause] + (rx/of {:file-id (:id file) + :error (ex-data cause)}))))) + (defn export-files - "Start files exportation process" - [& {:keys [type files]}] + "Start files exportation process. + + Every file is queued up front, even the ones whose job is only created + once the previous one is over, so a multiple export shows all of them + waiting from its first moment. + + The optional `:on-job` callback is invoked with `{:job-id ... + :file-id ...}` for every file job created, so the caller can track + them per file and cancel them while they run." + [& {:keys [type files on-job]}] (assert (check-export-files files) "expected a sequence of files") (assert (valid-types type) "expected valid export type") - (->> (rx/from files) - (rx/mapcat - (fn [file] - (->> (rp/cmd! ::sse/export-binfile {:file-id (:id file) - :version 3 - :type type}) - (rx/filter sse/end-of-stream?) - (rx/map sse/get-payload) - (rx/map (fn [uri] - {:file-id (:id file) - :uri uri - :filename (:name file)})) - (rx/catch (fn [cause] - (let [error (ex-data cause)] - (rx/of {:file-id (:id file) - :error error}))))))))) + (let [ws-conn (:ws-conn @st/state)] + (rx/concat + (rx/from (map (fn [file] {:file-id (:id file) :queued true}) files)) + (->> (rx/from files) + (rx/mapcat (fn [file] + (export-file ws-conn type file on-job))))))) ;;;;;;;;;;;;;;;;;;;;;; ;; Team Request diff --git a/frontend/src/app/main/data/imports.cljs b/frontend/src/app/main/data/imports.cljs new file mode 100644 index 0000000000..44cdd681d3 --- /dev/null +++ b/frontend/src/app/main/data/imports.cljs @@ -0,0 +1,294 @@ +;; 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 SUBSIDIARY SL + +(ns app.main.data.imports + "Reading a penpot package and importing it as a job. + + Both steps run in the main thread: reading the manifest of the package + is cheap (it scales with the number of entries, not with the size of the + file) and the work itself belongs to the server, so the only thing the + client has to be near is the websocket that reports the progress of the + job." + (:require + [app.common.exceptions :as ex] + [app.common.json :as json] + [app.common.schema :as sm] + [app.common.uuid :as uuid] + [app.main.data.jobs :as dj] + [app.main.data.uploads :as uploads] + [app.main.repo :as rp] + [app.main.store :as st] + [app.util.http :as http] + [app.util.i18n :as i18n :refer [tr]] + [app.util.zip :as uz] + [beicon.v2.core :as rx] + [cuerdas.core :as str])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; MESSAGES OF A FAILURE +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- resolve-hint + "The hint or the explanation of an error, trimmed, or nil." + [error] + (let [data (if (map? error) error (ex-data error))] + (or (some-> (:hint data) str/trim not-empty) + (some-> (:explain data) str/trim not-empty)))) + +(defn- cause-message + "The message of a failure of the transport: the hint the server sent, + then the explanation, then the message of the cause and finally the + generic one. The wrapper of an aborted stream says nothing useful, so it + is skipped." + [cause default] + (let [message (some-> (ex-message cause) str/trim)] + (or (resolve-hint cause) + (when-not (or (str/blank? message) (= message "stream exception")) + message) + default))) + +(defn- job-message + "The message of a job that failed." + [error] + (or (resolve-hint error) (tr "labels.error"))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; READING A PACKAGE +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- slurp-uri + [uri response-type] + (->> (http/send! {:uri uri :response-type response-type :method :get}) + (rx/map :body))) + +(defn- parse-mtype + "The mime type behind the first bytes of a file, guessed the same way the + browser would: only a zip can be a penpot package of version 3." + [ba] + (let [u8 (js/Uint8Array. ba 0 4) + sg (areduce u8 i ret "" (str ret (if (zero? i) "" " ") (.toString (aget u8 i) 8)))] + (case sg + "120 113 3 4" "application/zip" + "1 13 32 206" "application/octet-stream" + "other"))) + +(def ^:private schema:manifest + [:map {:title "Manifest"} + [:type :string] + [:files + [:vector + [:map + [:id ::sm/uuid] + [:name :string]]]]]) + +(def ^:private decode-manifest + (sm/decoder schema:manifest sm/json-transformer)) + +(defn- read-zip-manifest + [zip-reader] + (->> (rx/from (uz/get-entry zip-reader "manifest.json")) + (rx/mapcat (fn [entry] + (if (nil? entry) + (rx/throw (ex/error :type :validation + :code :invalid-penpot-file + :hint "Not a valid Penpot file: manifest.json is missing")) + (uz/read-as-text entry)))) + (rx/map json/decode))) + +(defn- analyze-file + "The entries of one file the user dropped: one per file of a version 3 + package, one for a version 1 file, and an error for anything else." + [{:keys [uri] :as file}] + (let [stream (->> (slurp-uri uri :buffer) + (rx/merge-map + (fn [body] + (let [mtype (parse-mtype body)] + (cond + (= "application/zip" mtype) + (let [zip-reader (uz/reader body)] + (->> (read-zip-manifest zip-reader) + (rx/map + (fn [manifest] + (if (= (:type manifest) "penpot/export-files") + (let [manifest (decode-manifest manifest)] + (assoc file :type :binfile-v3 :files (:files manifest))) + (assoc file :type :unknown)))) + (rx/finalize (partial uz/close zip-reader)))) + + (= "application/octet-stream" mtype) + (rx/of (assoc file :type :binfile-v1)) + + :else + (rx/of (assoc file :type :unknown)))))) + (rx/share))] + + (->> (rx/merge + (->> stream + (rx/filter (fn [entry] (= :binfile-v1 (:type entry)))) + (rx/map (fn [entry] + (let [file-id (uuid/next)] + (-> entry + (assoc :file-id file-id) + (assoc :name (:name file)) + (assoc :status :success)))))) + + (->> stream + (rx/filter (fn [entry] (= :binfile-v3 (:type entry)))) + (rx/merge-map (fn [{:keys [files] :as entry}] + (->> (rx/from files) + (rx/map (fn [file] + (-> entry + (dissoc :files) + (assoc :name (:name file)) + (assoc :file-id (:id file)) + (assoc :status :success)))))))) + + (->> stream + (rx/filter (fn [data] (= :unknown (:type data)))) + (rx/map (fn [_] + {:uri (:uri file) + :status :error + :error (tr "dashboard.import.analyze-error")})))) + + (rx/catch (fn [cause] + (let [error (cause-message cause (tr "dashboard.import.analyze-error"))] + (rx/of (assoc file :error error :status :error)))))))) + +(defn analyze + "Read the manifest of every file the user dropped and answer with the + entries the wizard shows: one per file of a package." + [files] + (->> (rx/from files) + (rx/merge-map analyze-file))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; IMPORTING A PACKAGE +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- packages + "The packages to import: a version 1 file is one package of one entry, + and the entries of a version 3 file share their upload, which the uri + of the dropped file identifies." + [entries] + (concat + (->> entries + (filter #(= :binfile-v1 (:type %))) + (map (fn [entry] + {:uri (:uri entry) + :version 1 + :name (str/replace (:name entry) #".penpot$" "") + :entries [entry]}))) + + (->> entries + (filter #(= :binfile-v3 (:type %))) + (group-by :uri) + (map (fn [[uri entries]] + {:uri uri + :version 3 + :name (:name (first entries)) + :entries entries}))))) + +(defn- entry-messages + "The message of every entry of a package: the job works on the package, + and each of its entries shows the same step." + [entries message] + (map (fn [entry] (assoc message :file-id (:file-id entry))) entries)) + +(defn- upload-milestone + "The milestone of the upload phase: the chunks done out of the total. + It is built on the client, never comes from the backend, and reuses + the milestone shape so the wizard shows it like any other step." + [{:keys [current total]}] + {:stage :upload :counters {:upload {:current current :total total}}}) + +(defn- import-package + "One package as a job: the blob is uploaded in chunks, the job is created + with the upload and followed, and its outcome becomes the messages of + the wizard. + + `on-job` is an optional callback invoked with the id of the created + job, so the caller can cancel it while it runs." + [ws-conn project-id resolutions {:keys [uri version name entries]} on-job] + (let [upload-events (rx/subject) + upload-ended? (atom false) + finish-upload! (fn [] + (when (compare-and-set! upload-ended? false true) + (rx/end! upload-events))) + report-upload! (fn [progress] + (doseq [message (entry-messages entries {:status :progress + :progress (upload-milestone progress)})] + (rx/push! upload-events message)))] + (rx/merge + upload-events + (->> (slurp-uri uri :blob) + (rx/mapcat (fn [blob] + (->> (uploads/upload-blob-chunked blob :on-progress report-upload!) + (rx/tap (fn [_] (finish-upload!)))))) + (rx/mapcat (fn [{:keys [session-id]}] + (rp/cmd! :create-import-binfile-job + {:params {:project-id project-id + :name name + :version version} + :upload-id session-id}))) + (rx/mapcat (fn [{job-id :id}] + (when (fn? on-job) + (on-job job-id)) + ;; the job exists but may wait for a worker: tell + ;; every entry it is queued before following it + (rx/concat + (rx/from (entry-messages entries {:status :queued})) + (->> (dj/watch-job ws-conn job-id) + (rx/mapcat (fn [{:keys [kind status result error] :as emission}] + (cond + (= "completed" status) + (do + (when-let [resolution (not-empty (:resolution result))] + (vswap! resolutions merge resolution)) + (rx/from (entry-messages entries {:status :finish}))) + + (= "failed" status) + (rx/from (entry-messages entries {:status :error + :error (job-message error)})) + + (= "cancelled" status) + (rx/from (entry-messages entries {:status :error + :error (tr "jobs.import-cancelled")})) + + (= :progress kind) + (rx/from (entry-messages entries {:status :progress + :progress (:payload emission)})) + + ;; a worker picked it up (or will + ;; retry it): no milestone yet + (contains? #{:start :retry} kind) + (rx/from (entry-messages entries {:status :started})) + + :else + (rx/empty)))))))) + (rx/catch (fn [cause] + (finish-upload!) + (rx/from (entry-messages entries {:status :error + :error (cause-message cause (tr "labels.error"))})))))))) + +(defn import-files + "Import the entries the wizard analyzed, one job per package. + + Answers with the messages the wizard already understands: the step of + the job for every entry (progress, finish or error) and, once every + package is over, the resolution of the libraries they brought. + + The optional `:on-job` callback is invoked with the id of every + package job created, so the caller can cancel them while they run." + [{:keys [project-id entries on-job]}] + (let [ws-conn (:ws-conn @st/state) + resolutions (volatile! {}) + importing (->> (rx/from (packages entries)) + (rx/merge-map (fn [package] + (import-package ws-conn project-id resolutions package on-job))))] + (rx/concat importing + (->> (rx/defer #(rx/of @resolutions)) + (rx/map (fn [resolution] + {:libraries-resolution resolution})))))) diff --git a/frontend/src/app/main/data/jobs.cljs b/frontend/src/app/main/data/jobs.cljs new file mode 100644 index 0000000000..9ffc801512 --- /dev/null +++ b/frontend/src/app/main/data/jobs.cljs @@ -0,0 +1,94 @@ +;; 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 SUBSIDIARY SL + +(ns app.main.data.jobs + "Following a job the client created. + + A running job publishes its progress as `:job-event` messages on the + websocket of the profile and keeps its outcome in its own row. This + namespace joins both: `watch-job` emits every event of the job and one + final outcome, read from the row and not from the event, because the + closing event carries only the outcome." + (:require + [app.main.repo :as rp] + [app.util.websocket :as ws] + [beicon.v2.core :as rx])) + +(def ^:private terminal-statuses + #{"completed" "failed" "cancelled"}) + +(defn- events-of + "The events of one job, as they arrive on the websocket, with the + milestone of the event under `:payload`." + [ws-conn job-id] + (->> (ws/get-rcv-stream ws-conn) + (rx/filter ws/message-event?) + (rx/map :payload) + (rx/filter #(and (= :job-event (:type %)) + (= job-id (:job-id %)))) + (rx/map (fn [event] + {:kind (keyword (:kind event)) + :payload (:payload event)})))) + +(defn- outcome-of + "The row of a job as the outcome of the stream, nil while it is not + over. A job that failed answers with the public error of the row." + [job] + (when (contains? terminal-statuses (:status job)) + (cond-> {:status (:status job)} + (some? (:result job)) + (assoc :result (:result job)) + + (some? (:error job)) + (assoc :error (:error job))))) + +(defn- read-outcome + [job-id] + (->> (rp/cmd! :get-job {:id job-id}) + (rx/map outcome-of) + (rx/filter some?))) + +(defn watch-job + "The lifecycle of `job-id` as an observable: every event it publishes + and one final outcome, after which the stream ends. + + The outcome is read from the row and not from the closing event: it is + read once when the stream starts and again on every reconnect of the + websocket, so a job that ended before this stream existed, or while the + socket was down, still answers. A read that fails reaches the caller, + which is the only one that can tell a transient failure from a job that + is gone. + + Emissions: + + - `{:kind :progress :payload {:stage ... :counters {...}}}` per event, + with the kind of the event (`:progress`, `:start`, `:retry`) + - `{:status \"completed\" :result {...}}` or `{:status \"failed\" + :error {...}}` or `{:status \"cancelled\"}` once, and then the + stream completes" + [ws-conn job-id] + (let [events (events-of ws-conn job-id) + progress (->> events + (rx/filter #(not= :end (:kind %)))) + triggers (rx/merge (->> (ws/get-rcv-stream ws-conn) + (rx/filter ws/opened-event?)) + (->> events + (rx/filter #(= :end (:kind %))))) + outcomes (->> (rx/merge (rx/of ::start) triggers) + (rx/mapcat (fn [_] (read-outcome job-id))) + (rx/take 1) + (rx/share))] + (rx/merge (rx/take-until outcomes progress) + outcomes))) + +(defn cancel-job + "Ask the server to cancel a job the caller created. Fire and forget: + failures are ignored because the job may have just finished on its + own." + [job-id] + (->> (rp/cmd! :cancel-job {:id job-id}) + (rx/catch (fn [_] (rx/of nil))) + (rx/subs! (constantly nil) (constantly nil) (constantly nil)))) diff --git a/frontend/src/app/main/data/uploads.cljs b/frontend/src/app/main/data/uploads.cljs index 5ae47c477a..f18956bd7f 100644 --- a/frontend/src/app/main/data/uploads.cljs +++ b/frontend/src/app/main/data/uploads.cljs @@ -11,7 +11,7 @@ by any feature that needs to upload large binary blobs: 1. create-upload-session – obtain a session-id - 2. upload-chunk – upload each slice (max-parallel-chunk-uploads in-flight) + 2. upload-chunk – upload each slice, one at a time 3. caller-specific step – e.g. assemble-file-media-object or import-binfile `upload-blob-chunked` drives steps 1 and 2 and emits the completed @@ -24,18 +24,13 @@ [app.main.repo :as rp] [beicon.v2.core :as rx])) -(def ^:private max-parallel-chunk-uploads - "Maximum number of chunk upload requests that may be in-flight at the - same time within a single chunked upload session." - 2) - (defn upload-blob-chunked "Uploads `blob` via the three-step chunked session API. Steps performed: 1. Creates an upload session (`create-upload-session`). - 2. Slices `blob` and uploads every chunk (`upload-chunk`), - with at most `max-parallel-chunk-uploads` concurrent requests. + 2. Slices `blob` and uploads every chunk (`upload-chunk`), one at a + time so the progress of the file is a straight line. Returns an observable that emits exactly one map: `{:session-id }` @@ -43,8 +38,11 @@ The caller is responsible for the final step (assemble / import). The optional `opts` map accepts: - `:chunk-size` – size in bytes of each chunk (default: `cf/upload-chunk-size`, 25 MiB)." - [blob & {:keys [chunk-size] :or {chunk-size cf/upload-chunk-size}}] + `:chunk-size` – size in bytes of each chunk (default: `cf/upload-chunk-size`, 25 MiB). + `:on-progress` – a fn called after every uploaded chunk with + `{:current :total }`, so the + caller can show the upload while it runs." + [blob & {:keys [chunk-size on-progress] :or {chunk-size cf/upload-chunk-size}}] (let [total-size (.-size blob) total-chunks (js/Math.ceil (/ total-size chunk-size))] (->> (rp/cmd! :create-upload-session @@ -53,17 +51,22 @@ (fn [{raw-session-id :session-id}] (let [session-id (cond-> raw-session-id (string? raw-session-id) uuid/uuid) + uploaded (atom 0) chunk-uploads (->> (range total-chunks) (map (fn [idx] (let [start (* idx chunk-size) end (min (+ start chunk-size) total-size) chunk (.slice blob start end)] - (rp/cmd! :upload-chunk - {:session-id session-id - :index idx - :content (list chunk (dm/str "chunk-" idx))})))))] + (->> (rp/cmd! :upload-chunk + {:session-id session-id + :index idx + :content (list chunk (dm/str "chunk-" idx))}) + (rx/tap (fn [_] + (when (fn? on-progress) + (on-progress {:current (swap! uploaded inc) + :total total-chunks})))))))))] (->> (rx/from chunk-uploads) - (rx/merge-all max-parallel-chunk-uploads) + (rx/concat-all) (rx/last) (rx/map (fn [_] {:session-id session-id}))))))))) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 01ce9e4c01..938e6aaa16 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -159,6 +159,7 @@ {:stream? true} :export-binfile {:response-type :blob} + :retrieve-list-of-builtin-templates {:query-params :all}}) (defn- send! diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index dd459da077..e03d67dbe9 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -13,6 +13,8 @@ [app.common.logging :as log] [app.main.data.dashboard :as dd] [app.main.data.event :as ev] + [app.main.data.imports :as imp] + [app.main.data.jobs :as dj] [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] [app.main.repo :as rp] @@ -29,7 +31,7 @@ [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] [app.main.ui.ds.product.loader :refer [loader*]] [app.main.ui.icons :as deprecated-icon] - [app.main.worker :as mw] + [app.main.ui.jobs.progress :as jp] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -108,45 +110,21 @@ (if (= (:file-id entry) (:file-id message)) (let [status (case (:status message) :progress :import-progress - :finish :import-success - :error :import-error)] - (-> entry - (assoc :progress (:progress message)) - (assoc :status status) - (assoc :error (:error message)) - (d/without-nils))) + :started :import-progress + :queued :import-queued + :finish :import-success + :error :import-error)] + ;; a message only carries what it knows: anything it does + ;; not bring (a stale milestone, an old error) is dropped + ;; instead of lingering on the row + (cond-> (assoc entry :status status) + (contains? message :progress) (assoc :progress (:progress message)) + (not (contains? message :progress)) (dissoc :progress) + (contains? message :error) (assoc :error (:error message)) + (not (contains? message :error)) (dissoc :error))) entry)) entries)) -(defn- parse-progress-message - [message] - (case (:type message) - :upload-data - (tr "dashboard.import.progress.upload-data" (:current message) (:total message)) - - :upload-media - (tr "dashboard.import.progress.upload-media" (:file message)) - - :process-page - (tr "dashboard.import.progress.process-page" (:file message)) - - :process-colors - (tr "dashboard.import.progress.process-colors") - - :process-typographies - (tr "dashboard.import.progress.process-typographies") - - :process-media - (tr "dashboard.import.progress.process-media") - - :process-components - (tr "dashboard.import.progress.process-components") - - :process-deleted-components - (tr "dashboard.import.progress.process-components") - - "")) - (defn- has-status-analyze? [item] (= (:status item) :analyze)) @@ -180,40 +158,32 @@ (defn- analyze-entries [state entries] - (let [features (get @st/state :features)] - (->> (mw/ask-many! - {:cmd :analyze-import - :files entries - :features features}) - (rx/mapcat #(rx/delay emit-delay (rx/of %))) - (rx/filter some?) - (rx/subs! - (fn [message] - (when (some? (:error message)) - (st/emit! (ev/event {::ev/name "import-files-error" - :error (:error message)}))) - (swap! state update-with-analyze-result message)))))) + (->> (imp/analyze entries) + (rx/mapcat #(rx/delay emit-delay (rx/of %))) + (rx/filter some?) + (rx/subs! + (fn [message] + (when (some? (:error message)) + (st/emit! (ev/event {::ev/name "import-files-error" + :error (:error message)}))) + (swap! state update-with-analyze-result message))))) (defn- import-files - [state library-resolution-data* project-id entries] + [state library-resolution-data* project-id entries on-job] (st/emit! (ev/event {::ev/name "import-files" :num-files (count entries)})) - (let [features (get @st/state :features)] - (->> (mw/ask-many! - {:cmd :import-files - :project-id project-id - :files entries - :features features}) - (rx/filter some?) - (rx/subs! - (fn [message] - ;; Capture library-resolution data if present (same for all - ;; entries from the same zip, so first one wins) - (if-let [resolution (-> (:libraries-resolution message) - (not-empty))] - (reset! library-resolution-data* resolution) - (swap! state update-entry-status message))))))) + (->> (imp/import-files {:project-id project-id + :entries entries + :on-job on-job}) + (rx/filter some?) + (rx/subs! + (fn [message] + ;; the resolution of the libraries of every package arrives once, + ;; when all of them are over + (if-let [resolution (-> (:libraries-resolution message) (not-empty))] + (reset! library-resolution-data* resolution) + (swap! state update-entry-status message)))))) (mf/defc import-entry* {::mf/memo true @@ -224,6 +194,7 @@ format (:type entry) loading? (or (= :analyze status) + (= :import-queued status) (= :import-progress status) (and is-progress (= :import-ready status))) analyze-error? (= :analyze-error status) @@ -336,24 +307,27 @@ [:> text* {:class (stl/css :error-message) :as "span" :typography t/body-small} - ;; backend-provided error key, dynamic by design - #_{:clj-kondo/ignore [:penpot/tr-dynamic]} + ;; the message is user-facing text already (a hint or a translated + ;; label), never a key (if (some? (:error entry)) - (tr (:error entry)) + (:error entry) (tr "dashboard.import.analyze-error"))] import-error? [:> text* {:class (stl/css :error-message) :as "span" :typography t/body-small} - ;; backend-provided error key, dynamic by design - #_{:clj-kondo/ignore [:penpot/tr-dynamic]} (if (some? (:error entry)) - (tr (:error entry)) + (:error entry) (tr "labels.error"))] + (and (= :import-queued status) (not import-success?)) + [:div {:class (stl/css :progress-message)} + (tr "jobs.queued")] + (and (not import-success?) (some? progress)) - [:div {:class (stl/css :progress-message)} (parse-progress-message progress)]) + [:div {:class (stl/css :progress-message)} + (jp/milestone-text progress :file? true)]) ;; This is legacy code, will be removed when legacy-zip format is removed [:div {:class (stl/css :linked-libraries)} @@ -577,7 +551,7 @@ {::mf/private true} [{:keys [entries template status errors? import-success-total auto-linked-count edition on-edit on-change on-delete - on-cancel on-continue on-accept pending-analysis?]}] + on-cancel on-cancel-import on-continue on-accept pending-analysis?]}] [:* [:div {:class (stl/css :modal-content)} (when (and (= :analyze status) errors?) @@ -639,13 +613,7 @@ (when (some? template) [:> import-entry* {:entry (assoc template :status status) - :can-be-deleted false}]) - - (when (= :import-progress status) - [:div {:class (stl/css :status-message) - :role "status" - :aria-live "polite"} - (tr "labels.uploading-file")])] + :can-be-deleted false}])] [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} @@ -664,11 +632,16 @@ (tr "labels.continue")] :import-progress - [:> button* {:class (stl/css :accept-btn) - :variant "primary" - :disabled true - :on-click on-accept} - (tr "labels.accept")] + [:* + [:> button* {:class (stl/css :cancel-button) + :variant "secondary" + :on-click on-cancel-import} + (tr "labels.cancel")] + [:> button* {:class (stl/css :accept-btn) + :variant "primary" + :disabled true + :on-click on-accept} + (tr "labels.accept")]] (:import-success :import-error) [:> button* {:class (stl/css :accept-btn) @@ -749,6 +722,13 @@ status* (mf/use-state :analyze) status (deref status*) + ;; Jobs created while importing, to cancel them on demand, and + ;; the subscription to their messages, to stop listening. + jobs* (mf/use-state #{}) + jobs (deref jobs*) + sub* (mf/use-state nil) + sub (deref sub*) + edition* (mf/use-state nil) edition (deref edition*) @@ -795,8 +775,10 @@ (mf/deps entries) (fn [] (let [entries (filterv has-status-ready? entries)] + (reset! jobs* #{}) (reset! status* :import-progress) - (import-files state* resolution* project-id entries)))) + (reset! sub* (import-files state* resolution* project-id entries + #(swap! jobs* conj %)))))) continue-template (mf/use-fn @@ -841,13 +823,33 @@ (fn [file-id] (swap! state* remove-entry file-id))) + on-cancel-import + (mf/use-fn + (mf/deps jobs sub) + (fn [event] + (dom/prevent-default event) + ;; stop listening first, so no late message repaints the + ;; entries of a dialog that is going away + (when (some? sub) + (rx/dispose! sub)) + ;; a job may have just finished on its own: failures are + ;; ignored and an empty run cancels nothing + (run! dj/cancel-job jobs) + (reset! jobs* #{}) + (reset! sub* nil) + (st/emit! (modal/hide)))) + on-cancel (mf/use-fn - (mf/deps edition) + (mf/deps edition status on-cancel-import) (fn [event] (when (nil? edition) - (dom/prevent-default event) - (st/emit! (modal/hide))))) + (if (= :import-progress status) + ;; closing mid-import stops the jobs, like Cancel + (on-cancel-import event) + (do + (dom/prevent-default event) + (st/emit! (modal/hide))))))) on-continue (mf/use-fn @@ -1013,6 +1015,7 @@ :on-change on-entry-change :on-delete on-entry-delete :on-cancel on-cancel + :on-cancel-import on-cancel-import :on-continue on-continue :on-accept on-accept :pending-analysis? pending-analysis?}] diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index f6765642b7..eafdc1ecc6 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -34,7 +34,7 @@ background-color: var(--color-background-primary); border: $b-2 solid var(--color-background-quaternary); min-block-size: $sz-192; - inline-size: $sz-512; + inline-size: px2rem(640); max-block-size: px2rem(800); } @@ -64,13 +64,6 @@ inset-inline-end: px2rem(6); } -.status-message { - @include use-typography("body-small"); - - color: var(--color-foreground-primary); - font-style: italic; -} - .action-buttons { display: flex; justify-content: flex-end; diff --git a/frontend/src/app/main/ui/exports/files.cljs b/frontend/src/app/main/ui/exports/files.cljs index 92a458e772..a36545fb95 100644 --- a/frontend/src/app/main/ui/exports/files.cljs +++ b/frontend/src/app/main/ui/exports/files.cljs @@ -11,6 +11,7 @@ [app.common.data.macros :as dm] [app.config :as cf] [app.main.data.exports.files :as fexp] + [app.main.data.jobs :as dj] [app.main.data.modal :as modal] [app.main.store :as st] [app.main.ui.ds.buttons.button :refer [button*]] @@ -20,6 +21,7 @@ [app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.ds.product.loader :refer [loader*]] + [app.main.ui.jobs.progress :as jp] [app.main.ui.notifications.context-notification :refer [context-notification]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -30,7 +32,17 @@ [files file-id] (mapv #(cond-> % (= file-id (:id %)) - (assoc :export-error? true + (assoc :export-error true + :loading false)) + files)) + +(defn- mark-file-cancelled + "The job of a file was cancelled elsewhere: a neutral terminal state, + neither success nor error." + [files file-id] + (mapv #(cond-> % + (= file-id (:id %)) + (assoc :export-cancelled true :loading false)) files)) @@ -38,10 +50,27 @@ [files file-id] (mapv #(cond-> % (= file-id (:id %)) - (assoc :export-success? true + (assoc :export-success true :loading false)) files)) +(defn- mark-file-progress + "The milestone the job of a file is in, to show it while it runs." + [files file-id progress] + (mapv #(cond-> % + (= file-id (:id %)) + (assoc :progress progress + :queued false)) + files)) + +(defn- mark-file-queued + "The job of a file exists but no worker picked it up yet." + [files file-id] + (mapv #(cond-> % + (= file-id (:id %)) + (assoc :queued true)) + files)) + (defn- initialize-state "Initialize export dialog state" [files] @@ -54,26 +83,52 @@ {::mf/private true} [{:keys [file]}] (let [level (cond - (:export-success? file) :success - (:export-error? file) :error - :else :info)] + (:export-success file) :success + (:export-error file) :error + (:export-cancelled file) :warning + :else :info)] [:div {:class (stl/css-case :file-entry true :loading (:loading file) - :success (:export-success? file) - :error (:export-error? file))} + :success (:export-success file) + :error (:export-error file))} (if (:loading file) - [:div {:class (stl/css :file-name)} - [:> loader* {:width 26 - :title (tr "labels.loading")}] - [:> text* {:class (stl/css :file-name-label) - :as "span" - :typography t/body-large} - (:name file)]] + [:* + [:div {:class (stl/css :file-name)} + [:> loader* {:width 26 + :title (tr "labels.loading")}] + [:> text* {:class (stl/css :file-name-label) + :as "span" + :typography t/body-large} + (:name file)]] - [:> context-notification {:level level - :content (:name file)}])])) + ;; while the file is being produced: waiting for a worker, then + ;; the milestone of the job + (when (or (some? (:progress file)) (:queued file)) + (let [progress (:progress file)] + [:> text* {:class (stl/css :status-message) + :as "span" + :typography (if (some? progress) t/body-large t/body-medium) + :role "status" + :aria-live "polite"} + (if (some? progress) + (jp/milestone-text progress) + (tr "jobs.queued"))]))] + + [:* + [:> context-notification {:level level + :content (:name file)}] + + ;; a cancelled file is a neutral terminal state, but the label is + ;; what tells the user why no artifact came out + (when (:export-cancelled file) + [:> text* {:class (stl/css :status-message) + :as "span" + :typography t/body-medium + :role "status" + :aria-live "polite"} + (tr "jobs.export-cancelled")])])])) (mf/defc export-dialog {::mf/register modal/components @@ -90,25 +145,75 @@ selected (:selected state) status (:status state) + ;; Jobs still running, as file-id -> job-id, to cancel them on + ;; demand; a file that reaches its outcome leaves the map, so + ;; closing a finished export cancels nothing. And the + ;; subscription to their messages, to stop listening. + jobs* (mf/use-state {}) + jobs (deref jobs*) + sub* (mf/use-state nil) + sub (deref sub*) + start-export (mf/use-fn (mf/deps team-id selected files) (fn [] (swap! state* assoc :status :exporting) - (->> (fexp/export-files :files files :type selected) - (rx/subs! - (fn [{:keys [file-id error filename uri] :as result}] - (if error - (swap! state* update :files mark-file-error file-id) - (do - (swap! state* update :files mark-file-success file-id) - (dom/trigger-download-uri filename "application/penpot" uri)))))))) + (reset! jobs* {}) + (reset! sub* (->> (fexp/export-files :files files :type selected + :on-job #(swap! jobs* assoc (:file-id %) (:job-id %))) + (rx/subs! + (fn [{:keys [file-id error filename uri progress queued + cancelled]}] + (cond + (some? progress) + (swap! state* update :files mark-file-progress file-id progress) + + (true? queued) + (swap! state* update :files mark-file-queued file-id) + + (some? error) + (swap! jobs* dissoc file-id) + (swap! state* update :files mark-file-error file-id) + + (true? cancelled) + (swap! jobs* dissoc file-id) + (swap! state* update :files mark-file-cancelled file-id) + + ;; only a message carrying the artifact + ;; downloads: anything else is ignored + (some? uri) + (do + (swap! jobs* dissoc file-id) + (swap! state* update :files mark-file-success file-id) + (dom/trigger-download-uri filename "application/penpot" uri))))))))) + + on-cancel-export + (mf/use-fn + (mf/deps jobs sub) + (fn [event] + (dom/prevent-default event) + ;; stop listening first, so no late message repaints the + ;; entries of a dialog that is going away + (when (some? sub) + (rx/dispose! sub)) + ;; only the jobs still in flight are cancelled: finished ones + ;; already left the map, and failures are ignored because a job may have just finished on its own + (run! dj/cancel-job (vals jobs)) + (reset! jobs* {}) + (reset! sub* nil) + (st/emit! (modal/hide)))) on-cancel (mf/use-fn + (mf/deps status on-cancel-export) (fn [event] - (dom/prevent-default event) - (st/emit! (modal/hide)))) + (if (= :exporting status) + ;; closing mid-export stops the jobs, like Cancel + (on-cancel-export event) + (do + (dom/prevent-default event) + (st/emit! (modal/hide)))))) on-accept (mf/use-fn @@ -123,7 +228,7 @@ (let [type (-> (dom/get-target event) (dom/get-data "type") (keyword))] - (prn "AAA" selected type) + (swap! state* assoc :selected type))))] (mf/with-effect [has-libs?] @@ -208,16 +313,15 @@ [:* [:div {:class (stl/css :modal-content)} (for [file (:files state)] - [:> export-entry* {:file file :key (dm/str (:id file))}]) - - (when in-progress? - [:> text* {:as "span" :typography t/body-large :class (stl/css :status-message) - :role "status" - :aria-live "polite"} - (tr "labels.downloading-file")])] + [:> export-entry* {:file file :key (dm/str (:id file))}])] [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} + (when in-progress? + [:> button* {:variant "secondary" + :type "button" + :on-click on-cancel-export} + (tr "labels.cancel")]) [:> button* {:variant "primary" :type "button" :disabled in-progress? diff --git a/frontend/src/app/main/ui/jobs/progress.cljs b/frontend/src/app/main/ui/jobs/progress.cljs new file mode 100644 index 0000000000..83967aa978 --- /dev/null +++ b/frontend/src/app/main/ui/jobs/progress.cljs @@ -0,0 +1,69 @@ +;; 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 SUBSIDIARY SL + +(ns app.main.ui.jobs.progress + "How a milestone of a job is written in a row: the stage in course and + its counter. + + The stage is a stable key of the vocabulary of the worker, never a + display string, so it is resolved here with a `case` of literal `tr` + calls: a stage this client does not know falls back to a generic label + instead of breaking the row or showing the raw key." + (:require + [app.util.i18n :as i18n :refer [tr]])) + +(defn- stage-label + [stage] + (case stage + :files (tr "jobs.progress.stage.files") + :pages (tr "jobs.progress.stage.pages") + :media (tr "jobs.progress.stage.media") + :thumbnails (tr "jobs.progress.stage.thumbnails") + :components (tr "jobs.progress.stage.components") + :colors (tr "jobs.progress.stage.colors") + :typographies (tr "jobs.progress.stage.typographies") + :tokens (tr "jobs.progress.stage.tokens") + :storage-objects (tr "jobs.progress.stage.storage-objects") + :relations (tr "jobs.progress.stage.relations") + :metadata (tr "jobs.progress.stage.metadata") + :manifest (tr "jobs.progress.stage.manifest") + :upload (tr "jobs.progress.stage.upload") + (tr "jobs.progress.stage.unknown"))) + +(defn- counter-label + "The counter of one scope, `3/8` when the worker knows the total and the + bare current when it does not." + [{:keys [current total]}] + (when (some? current) + (if (some? total) + (str current "/" total) + (str current)))) + +(defn- upload-label + "The upload of a file counts chunks, not units of work, so it is written + as the percentage of the file already sent instead of a counter." + [{:keys [current total]}] + (when (and (some? current) (some? total) (pos? total)) + (str "(" (js/Math.round (* 100 (/ current total))) "%)"))) + +(defn milestone-text + "The text of a milestone: the stage in course and its counter, and with + `:file?` the file in course behind it, when the milestone carries its + counter. The upload stage is the exception: it counts chunks, so it is + written as a percentage of the file." + [milestone & {:keys [file?]}] + (when-some [stage (:stage milestone)] + (let [counters (:counters milestone) + own (if (= :upload stage) + (upload-label (get counters :upload)) + (counter-label (get counters stage)))] + (str (stage-label stage) + (when (some? own) + (str " " own)) + (when-let [files (and file? + (not= :files stage) + (counter-label (get counters :files)))] + (str " · " (stage-label :files) " " files)))))) diff --git a/frontend/src/app/worker.cljs b/frontend/src/app/worker.cljs index da6f0c8a95..860abcff27 100644 --- a/frontend/src/app/worker.cljs +++ b/frontend/src/app/worker.cljs @@ -12,7 +12,6 @@ [app.common.types.objects-map] [app.util.object :as obj] [app.worker.impl :as impl] - [app.worker.import] [app.worker.index] [app.worker.messages :as wm] [app.worker.thumbnails] diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs deleted file mode 100644 index e8310efa89..0000000000 --- a/frontend/src/app/worker/import.cljs +++ /dev/null @@ -1,253 +0,0 @@ -;; 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 SUBSIDIARY SL - -(ns app.worker.import - (:refer-clojure :exclude [resolve]) - (:require - [app.common.exceptions :as ex] - [app.common.json :as json] - [app.common.logging :as log] - [app.common.schema :as sm] - [app.common.uuid :as uuid] - [app.main.data.uploads :as uploads] - [app.main.repo :as rp] - [app.util.http :as http] - [app.util.i18n :as i18n :refer [tr]] - [app.util.sse :as sse] - [app.util.zip :as uz] - [app.worker.impl :as impl] - [beicon.v2.core :as rx] - [cuerdas.core :as str])) - -(log/set-level! :warn) - -(defn- import-cause-message - "Prefer the server `:hint` (full text, e.g. SSE error payload), then `:explain` - when present; avoid the generic `stream exception` wrapper when a payload exists." - [cause default-msg] - (let [data (ex-data cause) - hint (some-> data :hint str/trim) - explain (some-> data :explain str/trim)] - (cond - (not (str/blank? hint)) hint - (not (str/blank? explain)) explain - :else - (let [msg (some-> (ex-message cause) str/trim)] - (if (or (str/blank? msg) (= msg "stream exception")) - default-msg - msg))))) - -;; Upload changes batches size -(def ^:const change-batch-size 100) - -(def conjv (fnil conj [])) - -(defn read-zip-manifest - [zip-reader] - (->> (rx/from (uz/get-entry zip-reader "manifest.json")) - (rx/mapcat (fn [entry] - (if (nil? entry) - (rx/throw (ex/error :type :validation - :code :invalid-penpot-file - :hint "Not a valid Penpot file: manifest.json is missing")) - (uz/read-as-text entry)))) - (rx/map json/decode))) - -(defn slurp-uri - ([uri] (slurp-uri uri :text)) - ([uri response-type] - (->> (http/send! - {:uri uri - :response-type response-type - :method :get}) - (rx/map :body)))) - -(defn parse-mtype [ba] - (let [u8 (js/Uint8Array. ba 0 4) - sg (areduce u8 i ret "" (str ret (if (zero? i) "" " ") (.toString (aget u8 i) 8)))] - (case sg - "120 113 3 4" "application/zip" - "1 13 32 206" "application/octet-stream" - "other"))) - -;; NOTE: this is a limited subset schema for the manifest file of -;; binfile-v3 format; is used for partially parse it and read the -;; files referenced inside the exported file - -(def ^:private schema:manifest - [:map {:title "Manifest"} - [:type :string] - [:files - [:vector - [:map - [:id ::sm/uuid] - [:name :string]]]]]) - -(def ^:private decode-manifest - (sm/decoder schema:manifest sm/json-transformer)) - -(defn analyze-file - [{:keys [uri] :as file}] - (let [stream (->> (slurp-uri uri :buffer) - (rx/merge-map - (fn [body] - (let [mtype (parse-mtype body)] - (cond - (= "application/zip" mtype) - (let [zip-reader (uz/reader body)] - (->> (read-zip-manifest zip-reader) - (rx/map - (fn [manifest] - (if (= (:type manifest) "penpot/export-files") - (let [manifest (decode-manifest manifest)] - (assoc file :type :binfile-v3 :files (:files manifest))) - (assoc file :type :unknown)))) - (rx/finalize (partial uz/close zip-reader)))) - - (= "application/octet-stream" mtype) - (rx/of (assoc file :type :binfile-v1)) - - :else - (rx/of (assoc file :type :unknown)))))) - - (rx/share))] - - (->> (rx/merge - (->> stream - (rx/filter (fn [entry] (= :binfile-v1 (:type entry)))) - (rx/map (fn [entry] - (let [file-id (uuid/next)] - (-> entry - (assoc :file-id file-id) - (assoc :name (:name file)) - (assoc :status :success)))))) - - (->> stream - (rx/filter (fn [entry] (= :binfile-v3 (:type entry)))) - (rx/merge-map (fn [{:keys [files] :as entry}] - (->> (rx/from files) - (rx/map (fn [file] - (-> entry - (dissoc :files) - (assoc :name (:name file)) - (assoc :file-id (:id file)) - (assoc :status :success)))))))) - - (->> stream - (rx/filter (fn [data] (= :unknown (:type data)))) - (rx/map (fn [_] - {:uri (:uri file) - :status :error - :error (tr "dashboard.import.analyze-error")})))) - - (rx/catch (fn [cause] - (let [error (import-cause-message cause (tr "dashboard.import.analyze-error"))] - (rx/of (assoc file :error error :status :error)))))))) - -(defmethod impl/handler :analyze-import - [{:keys [files]}] - (->> (rx/from files) - (rx/merge-map analyze-file))) - -(defn- import-blob-via-upload - "Fetches `uri` as a Blob, uploads it using the generic chunked-upload - session API and calls `import-binfile` with the resulting upload-id. - Returns an observable of SSE events from the import stream." - [uri {:keys [name version project-id]}] - (->> (slurp-uri uri :blob) - (rx/mapcat - (fn [blob] - (->> (uploads/upload-blob-chunked blob) - (rx/mapcat - (fn [{:keys [session-id]}] - (rp/cmd! ::sse/import-binfile - {:name name - :upload-id session-id - :version version - :project-id project-id})))))))) - -(defmethod impl/handler :import-files - [{:keys [project-id files]}] - (let [binfile-v1 (filter #(= :binfile-v1 (:type %)) files) - binfile-v3 (filter #(= :binfile-v3 (:type %)) files) - resolutions (volatile! {})] - - (rx/merge - (->> (rx/from binfile-v1) - (rx/merge-map - (fn [data] - (->> (import-blob-via-upload (:uri data) - {:name (str/replace (:name data) #".penpot$" "") - :version 1 - :project-id project-id}) - (rx/tap (fn [event] - (let [payload (sse/get-payload event) - type (sse/get-type event)] - (if (= type "progress") - (log/dbg :hint "import-binfile: progress" - :section (:section payload) - :name (:name payload)) - (log/dbg :hint "import-binfile: end"))))) - (rx/filter sse/end-of-stream?) - (rx/map (fn [_] - {:status :finish - :file-id (:file-id data)})) - (rx/catch - (fn [cause] - (log/error :hint "unexpected error on import process" - :project-id project-id - :cause cause) - (rx/of {:status :error - :error (import-cause-message cause (tr "labels.error")) - :file-id (:file-id data)}))))))) - - - (rx/concat - (->> (rx/from binfile-v3) - (rx/reduce (fn [result file] - (update result (:uri file) (fnil conj []) file)) - {}) - (rx/mapcat identity) - (rx/merge-map - (fn [[uri entries]] - (->> (import-blob-via-upload uri - {:name (-> entries first :name) - :version 3 - :project-id project-id}) - (rx/tap (fn [event] - (let [payload (sse/get-payload event) - type (sse/get-type event)] - (cond - (= type "progress") - (log/dbg :hint "import-binfile: progress" - :section (:section payload) - :name (:name payload)) - - :else - (log/dbg :hint "import-binfile: end"))))) - (rx/filter sse/end-of-stream?) - (rx/mapcat (fn [message] - (let [{:keys [resolution]} (sse/get-payload message)] - (when (seq resolution) - (vswap! resolutions merge resolution)) - (->> (rx/from entries) - (rx/map (fn [entry] - {:status :finish - :file-id (:file-id entry)})))))) - (rx/catch (fn [cause] - (log/error :hint "import-binfile: unexpected error on importing" - :project-id project-id - ::log/sync? true - :cause cause) - (let [err (import-cause-message cause (tr "labels.error"))] - (->> (rx/from entries) - (rx/map (fn [entry] - {:status :error - :error err - :file-id (:file-id entry)})))))))))) - (->> (rx/defer #(rx/of @resolutions)) - (rx/map (fn [resolutions] - {:libraries-resolution resolutions}))))))) diff --git a/frontend/test/frontend_tests/data/exports_files_test.cljs b/frontend/test/frontend_tests/data/exports_files_test.cljs new file mode 100644 index 0000000000..a979ac34d7 --- /dev/null +++ b/frontend/test/frontend_tests/data/exports_files_test.cljs @@ -0,0 +1,286 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.data.exports-files-test + "Exporting files as jobs: one file, one job and one answer, with the + progress of the job reported while it runs." + (:require + [app.common.uuid :as uuid] + [app.main.data.exports.files :as fexp] + [app.main.repo :as rp] + [app.main.store :as st] + [app.util.websocket :as ws] + [beicon.v2.core :as rx] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.async :as hva] + [frontend-tests.helpers.mock :as mock])) + +(defn- file + [name] + {:id (uuid/next) + :name name + :project-id (uuid/next) + :is-shared false}) + +(defn- job + [job-id status & {:as extra}] + (merge {:id job-id :status status :name "export-binfile"} extra)) + +(defn- event + [job-id kind payload] + {:type :job-event + :job-id job-id + :kind kind + :payload payload}) + +(defn- message + [payload] + {:type :message :payload payload}) + +(defn- fake-server + "The backend of the export: the creation answers with a fresh job the + test drives, and the read answers the row of that job." + [calls rows] + (mock/stub + (fn [id params] + (case id + :create-export-binfile-job + (let [job-id (uuid/next)] + (swap! calls conj {:cmd id :params params :job-id job-id}) + (swap! rows assoc job-id (job job-id "running")) + (rx/of {:id job-id})) + + :get-job + (do + (swap! calls conj {:cmd id :params params}) + (rx/of (get @rows (:id params)))) + + (rx/of nil))))) + +(def ^:private created? + (comp #{:create-export-binfile-job} :cmd)) + +(defn- answered? + "True when the file already has its answer: the artifact or an error." + [results file-id] + (some (fn [result] + (and (= file-id (:file-id result)) + (or (:uri result) (:error result)))) + results)) + +(t/deftest ^:async a-file-is-exported-as-its-own-job + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + target (file "My file")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + rp/cmd! (fake-server calls rows) + ws/get-rcv-stream (mock/stub (fn [_] ws-stream))} + + (let [done (hva/observe (fexp/export-files :files [target] :type :detach-libraries) + {:on-next #(swap! seen conj %)}) + job-id (:job-id (first @calls))] + + (t/testing "the file is exported by a job of its own" + (t/is (= :create-export-binfile-job (:cmd (first @calls)))) + (t/is (= {:params {:file-ids #{(:id target)} + :export-type :detach-libraries}} + (:params (first @calls))))) + + (t/testing "the milestone of the job is reported while it runs" + (rx/push! ws-stream + (message (event job-id :progress + {:stage :pages + :counters {:files {:current 1 :total 1} + :pages {:current 1 :total 2}}}))) + (await (hva/wait-for #(some :progress @seen) "the milestone")) + + (t/is (= {:file-id (:id target) + :progress {:stage :pages + :counters {:files {:current 1 :total 1} + :pages {:current 1 :total 2}}}} + (last @seen)))) + + (t/testing "the artifact of the job answers when it is over" + (swap! rows assoc job-id + (job job-id "completed" + :result {:resource-uri "http://assets/export" + :filename "export.penpot" + :mtype "application/zip" + :size 120})) + (rx/push! ws-stream (message (event job-id :end {:outcome "completed"}))) + (await done) + + (t/is (= {:file-id (:id target) + :uri "http://assets/export" + :filename "My file"} + (last @seen))))))))) + +(t/deftest ^:async a-failure-fails-only-its-own-file + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + one (file "First") + other (file "Second")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + rp/cmd! (fake-server calls rows) + ws/get-rcv-stream (mock/stub (fn [_] ws-stream))} + + (let [done (hva/observe (fexp/export-files :files [one other] :type :detach-libraries) + {:on-next #(swap! seen conj %)})] + + (t/testing "the first file answers with its artifact" + (let [job-id (:job-id (first (filter created? @calls)))] + (swap! rows assoc job-id + (job job-id "completed" :result {:resource-uri "http://assets/first"})) + (rx/push! ws-stream (message (event job-id :end {:outcome "completed"}))) + (await (hva/wait-for #(answered? @seen (:id one)) "the first answer")))) + + (t/testing "and the second one fails on its own" + (await (hva/wait-for #(= 2 (count (filter created? @calls))) "the second job")) + (let [job-id (:job-id (second (filter created? @calls)))] + (swap! rows assoc job-id + (job job-id "failed" :error {:type :internal + :code :boom + :hint "boom"})) + (rx/push! ws-stream (message (event job-id :end {:outcome "failed"}))) + (await done))) + + (t/testing "the failure does not touch the file that worked" + (t/is (= {:file-id (:id one) + :uri "http://assets/first" + :filename "First"} + (first (filter #(and (= (:id one) (:file-id %)) + (:uri %)) + @seen))))) + + (t/is (= {:file-id (:id other) + :error {:type :internal :code :boom :hint "boom"}} + (last @seen)))))))) + +(t/deftest ^:async the-created-jobs-reach-the-caller-tagged-with-their-file + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen-jobs (atom []) + one (file "First") + other (file "Second")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + rp/cmd! (fake-server calls rows) + ws/get-rcv-stream (mock/stub (fn [_] ws-stream))} + + (let [done (hva/observe (fexp/export-files :files [one other] + :type :detach-libraries + :on-job #(swap! seen-jobs conj %)) + {:on-next (constantly nil)})] + + (await (hva/wait-for #(= 1 (count (filter created? @calls))) "the first job")) + + (t/testing "the server recorded the creation with its job id" + (let [creates (filterv created? @calls)] + (t/is (= 1 (count creates))) + (t/is (= :create-export-binfile-job (:cmd (first creates)))) + (t/is (uuid? (:job-id (first creates)))))) + + (t/testing "the caller learns that same job tagged with its file" + (t/is (= [{:job-id (:job-id (first (filterv created? @calls))) + :file-id (:id one)}] + @seen-jobs))) + + ;; files export one after the other: the second job is only + ;; created once the first one is over + (let [{job-1 :job-id} (first @seen-jobs)] + (swap! rows assoc job-1 (job job-1 "completed" :result {:resource-uri "http://assets/first"})) + (rx/push! ws-stream (message (event job-1 :end {:outcome "completed"})))) + + (await (hva/wait-for #(= 2 (count (filter created? @calls))) "the second job")) + + (t/testing "the caller learns the second job too, tagged and in order" + (t/is (= 2 (count @seen-jobs))) + (t/is (= (:job-id (second (filterv created? @calls))) (:job-id (second @seen-jobs)))) + (t/is (= (:id other) (:file-id (second @seen-jobs))))) + + (let [{job-2 :job-id} (second @seen-jobs)] + (swap! rows assoc job-2 (job job-2 "completed" :result {:resource-uri "http://assets/second"})) + (rx/push! ws-stream (message (event job-2 :end {:outcome "completed"}))) + (await done))))))) + +(t/deftest ^:async every-file-is-queued-until-its-first-milestone + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + one (file "First") + other (file "Second")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + rp/cmd! (fake-server calls rows) + ws/get-rcv-stream (mock/stub (fn [_] ws-stream))} + + (let [done (hva/observe (fexp/export-files :files [one other] :type :detach-libraries) + {:on-next #(swap! seen conj %)})] + + (t/testing "every file queues up front, before any of its jobs exists" + (await (hva/wait-for #(= 2 (count (filter :queued @seen))) "the queued messages")) + (t/is (= [{:file-id (:id one) :queued true} + {:file-id (:id other) :queued true}] + (vec (filter :queued @seen))))) + + (t/testing "a worker picking a job up is not a milestone and ends nothing" + (let [job-id (:job-id (first (filter created? @calls)))] + (rx/push! ws-stream (message (event job-id :start {:attempt 1}))) + (t/is (not-any? :started @seen)))) + + ;; finish the first job so the second one is created and the run ends + (let [job-1 (:job-id (first (filter created? @calls)))] + (swap! rows assoc job-1 + (job job-1 "completed" :result {:resource-uri "http://assets/first"})) + (rx/push! ws-stream (message (event job-1 :end {:outcome "completed"})))) + + (await (hva/wait-for #(= 2 (count (filter created? @calls))) "the second job")) + + (let [job-2 (:job-id (second (filter created? @calls)))] + (swap! rows assoc job-2 + (job job-2 "completed" :result {:resource-uri "http://assets/second"})) + (rx/push! ws-stream (message (event job-2 :end {:outcome "completed"}))) + (await done))))))) + +(t/deftest ^:async a-cancelled-job-marks-the-file-cancelled-without-artifact + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + target (file "My file")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + rp/cmd! (fake-server calls rows) + ws/get-rcv-stream (mock/stub (fn [_] ws-stream))} + + (let [done (hva/observe (fexp/export-files :files [target] :type :detach-libraries) + {:on-next #(swap! seen conj %)}) + job-id (:job-id (first @calls))] + + (t/testing "the cancelled outcome is an explicit message, never an artifact" + (swap! rows assoc job-id (job job-id "cancelled")) + (rx/push! ws-stream (message (event job-id :end {:outcome "cancelled"}))) + (await done) + (t/is (= {:file-id (:id target) :cancelled true} (last @seen))) + (t/is (not-any? :uri @seen)))))))) diff --git a/frontend/test/frontend_tests/data/imports_test.cljs b/frontend/test/frontend_tests/data/imports_test.cljs new file mode 100644 index 0000000000..e73802fe38 --- /dev/null +++ b/frontend/test/frontend_tests/data/imports_test.cljs @@ -0,0 +1,533 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.data.imports-test + "Reading a penpot package and importing it as a job." + (:require + [app.common.json :as json] + [app.common.uuid :as uuid] + [app.main.data.imports :as imp] + [app.main.data.jobs :as dj] + [app.main.data.uploads :as uploads] + [app.main.repo :as rp] + [app.main.store :as st] + [app.util.http :as http] + [app.util.i18n :refer [tr]] + [app.util.websocket :as ws] + [app.util.zip :as uz] + [beicon.v2.core :as rx] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.async :as hva] + [frontend-tests.helpers.mock :as mock] + [promesa.core :as p])) + +(def ^:private project-id + (uuid/next)) + +(def ^:private uri + "blob:package") + +(def ^:private zip-magic + (js/Uint8Array. #js [0x50 0x4b 0x03 0x04])) + +(defn- entry + "An entry of the wizard, as the analysis leaves it." + [& {:keys [id name type] + :or {type :binfile-v3}}] + {:file-id (or id (uuid/next)) + :uri uri + :name (or name "My file") + :type type}) + +(defn- job + [job-id status & {:as extra}] + (merge {:id job-id :status status :name "import-binfile"} extra)) + +(defn- event + [job-id kind payload] + {:type :job-event + :job-id job-id + :kind kind + :payload payload}) + +(defn- message + [payload] + {:type :message :payload payload}) + +(defn- slurp-stub + "The transport of a request that answers with `body`." + [body] + (mock/stub (fn [_] (rx/of {:body body})))) + +(defn- upload-stub + "The chunked upload of a package. It is variadic, so it is a plain fn." + [session-id] + (fn [_ & _] (rx/of {:session-id session-id}))) + +(defn- upload-with-progress-stub + "The chunked upload of a package that reports two chunks before + emitting the session, so the test sees the upload phase. It is + variadic, so it is a plain fn." + [session-id] + (fn [_ & {:keys [on-progress]}] + (when (fn? on-progress) + (on-progress {:current 1 :total 2}) + (on-progress {:current 2 :total 2})) + (rx/of {:session-id session-id}))) + +(defn- ws-stub + [stream] + (mock/stub (fn [_] stream))) + +(defn- fake-server + "The backend of the import: the creation answers with a fresh job the + test drives, and the read answers the row of that job." + [calls rows create-row] + (mock/stub + (fn [id params] + (case id + :create-import-binfile-job + (let [job-id (uuid/next)] + (swap! calls conj {:cmd id :params params :job-id job-id}) + (swap! rows assoc job-id (create-row job-id)) + (rx/of {:id job-id})) + + :get-job + (do + (swap! calls conj {:cmd id :params params}) + (rx/of (get @rows (:id params)))) + + (rx/of nil))))) + +(defn- manifest + [& files] + (json/encode {:type "penpot/export-files" + :files (mapv (fn [[id name]] {:id id :name name}) files)})) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; READING A PACKAGE +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest ^:async a-package-without-a-manifest-is-rejected + (let [seen (atom [])] + + (await + (mock/with-mocks* + {http/send! (slurp-stub zip-magic) + uz/get-entry (mock/stub (fn [_ _] (p/resolved nil))) + uz/close (mock/stub (fn [_] nil))} + + (await (hva/observe (imp/analyze [{:uri uri :name "broken.penpot"}]) + {:on-next #(swap! seen conj %)})) + + (t/testing "the entry carries the error of the reader" + (t/is (= 1 (count @seen))) + (t/is (= :error (:status (first @seen)))) + (t/is (string? (:error (first @seen))))))))) + +(t/deftest ^:async a-version-3-package-answers-one-entry-per-file + (let [first-id (uuid/next) + second-id (uuid/next) + seen (atom [])] + + (await + (mock/with-mocks* + {http/send! (slurp-stub zip-magic) + uz/get-entry (mock/stub (fn [_ _] (p/resolved ::entry))) + uz/read-as-text (mock/stub (fn [_] (p/resolved (manifest [first-id "First"] + [second-id "Second"])))) + uz/close (mock/stub (fn [_] nil))} + + (await (hva/observe (imp/analyze [{:uri uri :name "package.penpot"}]) + {:on-next #(swap! seen conj %)})) + + (t/testing "every file of the manifest is an entry of the wizard" + (t/is (= [first-id second-id] (mapv :file-id @seen))) + (t/is (= ["First" "Second"] (mapv :name @seen))) + (t/is (every? #(= :binfile-v3 (:type %)) @seen)) + (t/is (every? #(= :success (:status %)) @seen))) + + (t/testing "all of them share the upload of the file that was dropped" + (t/is (every? #(= uri (:uri %)) @seen))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; IMPORTING A PACKAGE +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest ^:async a-package-is-imported-as-its-own-job + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + session-id (uuid/next) + resolution {:done [{:id (str (uuid/next)) :name "Published"}]} + one (entry :name "First") + other (entry :name "Second")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + http/send! (slurp-stub ::blob) + uploads/upload-blob-chunked (upload-stub session-id) + rp/cmd! (fake-server calls rows (fn [job-id] (job job-id "running"))) + ws/get-rcv-stream (ws-stub ws-stream)} + + (let [done (hva/observe (imp/import-files {:project-id project-id + :entries [one other]}) + {:on-next #(swap! seen conj %)}) + job-id (:job-id (first @calls))] + + (t/testing "the package is uploaded and imported as one job" + (t/is (= :create-import-binfile-job (:cmd (first @calls)))) + (t/is (= {:project-id project-id :name "First" :version 3} + (:params (:params (first @calls))))) + (t/is (= session-id (:upload-id (:params (first @calls)))))) + + (t/testing "the milestone of the job reaches every entry of the package" + (rx/push! ws-stream + (message (event job-id :progress + {:stage :pages + :counters {:files {:current 1 :total 2} + :pages {:current 1 :total 8}}}))) + (await (hva/wait-for #(= 2 (count (filter (comp #{:progress} :status) @seen))) + "the milestone")) + + (t/is (= #{(:file-id one) (:file-id other)} + (into #{} (comp (filter (comp #{:progress} :status)) (map :file-id)) @seen))) + + (t/is (every? (fn [message] + (= {:stage :pages + :counters {:files {:current 1 :total 2} + :pages {:current 1 :total 8}}} + (:progress message))) + (filter (comp #{:progress} :status) @seen)))) + + (t/testing "the entries finish with the resolution of the package" + (swap! rows assoc job-id + (job job-id "completed" + :result {:file-ids (mapv (comp str :file-id) [one other]) + :resolution resolution + :name "First" + :version 3})) + (rx/push! ws-stream (message (event job-id :end {:outcome "completed"}))) + (await done) + + (t/is (= #{(:file-id one) (:file-id other)} + (into #{} (comp (filter (comp #{:finish} :status)) (map :file-id)) @seen))) + + (t/is (= {:libraries-resolution resolution} (last @seen))))))))) + +(t/deftest ^:async the-job-reports-queued-then-started-before-progress + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + session-id (uuid/next) + one (entry :name "First") + other (entry :name "Second")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + http/send! (slurp-stub ::blob) + uploads/upload-blob-chunked (upload-stub session-id) + rp/cmd! (fake-server calls rows (fn [job-id] (job job-id "running"))) + ws/get-rcv-stream (ws-stub ws-stream)} + + (let [done (hva/observe (imp/import-files {:project-id project-id + :entries [one other]}) + {:on-next #(swap! seen conj %)}) + job-id (:job-id (first @calls))] + + (t/testing "every entry is queued while the job waits for a worker" + (await (hva/wait-for #(= 2 (count (filter (comp #{:queued} :status) @seen))) + "the queued messages")) + (t/is (= #{(:file-id one) (:file-id other)} + (into #{} (comp (filter (comp #{:queued} :status)) (map :file-id)) @seen)))) + + (t/testing "a started job moves the entries to progress without a milestone" + (rx/push! ws-stream (message (event job-id :start {:attempt 1}))) + (await (hva/wait-for #(= 2 (count (filter (comp #{:started} :status) @seen))) + "the started messages")) + (t/is (= #{(:file-id one) (:file-id other)} + (into #{} (comp (filter (comp #{:started} :status)) (map :file-id)) @seen)))) + + (t/testing "the milestone still arrives on top" + (rx/push! ws-stream + (message (event job-id :progress + {:stage :pages + :counters {:pages {:current 1 :total 8}}}))) + (await (hva/wait-for #(some :progress (filter (comp #{:progress} :status) @seen)) + "the milestone")) + (swap! rows assoc job-id + (job job-id "completed" + :result {:file-ids (mapv (comp str :file-id) [one other]) + :resolution {} + :name "First" + :version 3})) + (rx/push! ws-stream (message (event job-id :end {:outcome "completed"}))) + (await done))))))) + +(t/deftest ^:async a-failed-job-fails-its-entries + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + target (entry :name "Only")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + http/send! (slurp-stub ::blob) + uploads/upload-blob-chunked (upload-stub (uuid/next)) + rp/cmd! (fake-server calls rows (fn [job-id] (job job-id "running"))) + ws/get-rcv-stream (ws-stub ws-stream)} + + (let [done (hva/observe (imp/import-files {:project-id project-id + :entries [target]}) + {:on-next #(swap! seen conj %)}) + job-id (:job-id (first @calls))] + + (swap! rows assoc job-id + (job job-id "failed" :error {:type :internal + :code :boom + :hint "The package is broken"})) + (rx/push! ws-stream (message (event job-id :end {:outcome "failed"}))) + (await done) + + (t/testing "the entry fails with the message of the job" + (t/is (= [{:status :queued + :file-id (:file-id target)} + {:status :error + :file-id (:file-id target) + :error "The package is broken"} + {:libraries-resolution {}}] + @seen)))))))) + +(t/deftest ^:async a-package-that-cannot-be-created-fails-its-entries + (let [ws-stream (rx/subject) + seen (atom []) + target (entry :name "Only")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + http/send! (slurp-stub ::blob) + uploads/upload-blob-chunked (upload-stub (uuid/next)) + rp/cmd! (mock/stub (fn [_ _] + (rx/throw (ex-info "cannot create the job" + {:type :validation + :code :boom + :hint "Not now"})))) + ws/get-rcv-stream (ws-stub ws-stream)} + + (await (hva/observe (imp/import-files {:project-id project-id + :entries [target]}) + {:on-next #(swap! seen conj %)})) + + (t/testing "the failure reaches every entry of the package" + (t/is (= [{:status :error + :file-id (:file-id target) + :error "Not now"} + {:libraries-resolution {}}] + @seen))))))) + +(t/deftest ^:async a-job-that-already-ended-does-not-leave-the-entries-spinning + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + target (entry :name "Only")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + http/send! (slurp-stub ::blob) + uploads/upload-blob-chunked (upload-stub (uuid/next)) + ;; the job is over before the client reads it for the first time + rp/cmd! (fake-server calls rows + (fn [job-id] (job job-id "completed" :result {:resolution {}}))) + ws/get-rcv-stream (ws-stub ws-stream)} + + (await (hva/observe (imp/import-files {:project-id project-id + :entries [target]}) + {:on-next #(swap! seen conj %)})) + + (t/testing "the entry finishes without waiting for an event" + (t/is (= [{:status :queued + :file-id (:file-id target)} + {:status :finish + :file-id (:file-id target)} + {:libraries-resolution {}}] + @seen))))))) + +(t/deftest ^:async the-upload-reports-chunks-before-the-job-runs + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + session-id (uuid/next) + target (entry :name "Only")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + http/send! (slurp-stub ::blob) + uploads/upload-blob-chunked (upload-with-progress-stub session-id) + rp/cmd! (fake-server calls rows (fn [job-id] (job job-id "running"))) + ws/get-rcv-stream (ws-stub ws-stream)} + + (let [done (hva/observe (imp/import-files {:project-id project-id + :entries [target]}) + {:on-next #(swap! seen conj %)}) + job-id (:job-id (first @calls))] + + (await (hva/wait-for #(>= (count (filter (comp #{:progress} :status) @seen)) 2) + "the upload progress")) + + (t/testing "every uploaded chunk reaches the entry as an upload milestone" + (t/is (= [{:status :progress + :file-id (:file-id target) + :progress {:stage :upload + :counters {:upload {:current 1 :total 2}}}} + {:status :progress + :file-id (:file-id target) + :progress {:stage :upload + :counters {:upload {:current 2 :total 2}}}}] + (filterv (comp #{:progress} :status) @seen)))) + + (t/testing "the entry still finishes with the job" + (swap! rows assoc job-id (job job-id "completed" :result {:resolution {}})) + (rx/push! ws-stream (message (event job-id :end {:outcome "completed"}))) + (await done) + + (t/is (= {:libraries-resolution {}} (last @seen))) + (t/is (= :finish (:status (nth @seen 3)))))))))) + +(t/deftest ^:async a-cancelled-job-closes-its-entries-as-cancelled + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + target (entry :name "Only")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + http/send! (slurp-stub ::blob) + uploads/upload-blob-chunked (upload-stub (uuid/next)) + rp/cmd! (fake-server calls rows (fn [job-id] (job job-id "running"))) + ws/get-rcv-stream (ws-stub ws-stream)} + + (let [done (hva/observe (imp/import-files {:project-id project-id + :entries [target]}) + {:on-next #(swap! seen conj %)}) + job-id (:job-id (first @calls))] + + (t/testing "the cancelled ending terminates the stream" + (swap! rows assoc job-id (job job-id "cancelled")) + (rx/push! ws-stream (message (event job-id :end {:outcome "cancelled"}))) + (await done) + + (t/testing "the cancelled ending closes the entries and terminates the stream" + (t/is (= [{:status :queued + :file-id (:file-id target)} + {:status :error + :file-id (:file-id target) + :error (tr "jobs.import-cancelled")} + {:libraries-resolution {}}] + @seen))))))))) + +(t/deftest ^:async a-cancelled-job-closes-in-progress-entries-as-cancelled + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen (atom []) + target (entry :name "Only")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + http/send! (slurp-stub ::blob) + uploads/upload-blob-chunked (upload-stub (uuid/next)) + rp/cmd! (fake-server calls rows (fn [job-id] (job job-id "running"))) + ws/get-rcv-stream (ws-stub ws-stream)} + + (let [done (hva/observe (imp/import-files {:project-id project-id + :entries [target]}) + {:on-next #(swap! seen conj %)}) + job-id (:job-id (first @calls))] + + ;; drive the entries to progress first, so the cancel + ;; interrupts a run in course instead of a queued job + (rx/push! ws-stream (message (event job-id :progress {:stage :manifest}))) + (await (hva/wait-for #(some (fn [message] (= :progress (:status message))) @seen) + "the progress")) + + (t/testing "an external cancel closes every entry as cancelled" + (swap! rows assoc job-id (job job-id "cancelled")) + (rx/push! ws-stream (message (event job-id :end {:outcome "cancelled"}))) + (await done) + (t/is (= [{:status :queued + :file-id (:file-id target)} + {:status :progress + :file-id (:file-id target) + :progress {:stage :manifest}} + {:status :error + :file-id (:file-id target) + :error (tr "jobs.import-cancelled")} + {:libraries-resolution {}}] + @seen)))))))) + +(t/deftest ^:async the-created-job-id-reaches-the-caller + (let [ws-stream (rx/subject) + calls (atom []) + rows (atom {}) + seen-jobs (atom []) + target (entry :name "Only")] + + (await + (mock/with-mocks* + {st/state (atom {:ws-conn ws-stream}) + http/send! (slurp-stub ::blob) + uploads/upload-blob-chunked (upload-stub (uuid/next)) + rp/cmd! (fake-server calls rows (fn [job-id] (job job-id "running"))) + ws/get-rcv-stream (ws-stub ws-stream)} + + (let [done (hva/observe (imp/import-files {:project-id project-id + :entries [target] + :on-job #(swap! seen-jobs conj %)}) + {:on-next (constantly nil)}) + job-id (:job-id (first @calls))] + + (t/testing "the caller learns the id of the package job" + (t/is (= [job-id] @seen-jobs))) + + (swap! rows assoc job-id (job job-id "completed" :result {:resolution {}})) + (rx/push! ws-stream (message (event job-id :end {:outcome "completed"}))) + (await done)))))) + +(t/deftest ^:async cancel-job-asks-the-server-and-ignores-failures + (let [calls (atom []) + job-id (uuid/next)] + + (t/testing "the cancel command reaches the server" + (await + (mock/with-mocks* + {rp/cmd! (mock/stub (fn [id params] + (swap! calls conj {:cmd id :params params}) + (rx/of {:id job-id :status "cancelled"})))} + (dj/cancel-job job-id) + (await (hva/settle)) + (t/is (= [{:cmd :cancel-job :params {:id job-id}}] @calls))))) + + (t/testing "a failure is swallowed: the job may have just finished" + (await + (mock/with-mocks* + {rp/cmd! (mock/stub (fn [_ _] + (rx/throw (ex-info "gone" {}))))} + (dj/cancel-job job-id) + (await (hva/settle))))))) diff --git a/frontend/test/frontend_tests/data/jobs_test.cljs b/frontend/test/frontend_tests/data/jobs_test.cljs new file mode 100644 index 0000000000..996d9d1f0b --- /dev/null +++ b/frontend/test/frontend_tests/data/jobs_test.cljs @@ -0,0 +1,176 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.data.jobs-test + "Following a job: the progress the websocket publishes and the outcome + the row holds." + (:require + [app.common.uuid :as uuid] + [app.main.data.jobs :as dj] + [app.main.repo :as rp] + [app.util.http :as http] + [app.util.websocket :as ws] + [beicon.v2.core :as rx] + [cljs.test :as t :include-macros true] + [cuerdas.core :as str] + [frontend-tests.helpers.async :as hva] + [frontend-tests.helpers.mock :as mock])) + +(def ^:private job-id + (uuid/next)) + +(defn- job + "The row of a job, as `get-job` answers it." + [status & {:as extra}] + (merge {:id job-id :status status :name "export-binfile"} extra)) + +(defn- event + ([kind payload] (event job-id kind payload)) + ([id kind payload] + {:type :job-event + :job-id id + :kind kind + :payload payload})) + +(defn- message + [payload] + {:type :message :payload payload}) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; FOLLOWING A JOB +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(t/deftest ^:async progress-events-are-emitted-and-the-outcome-ends-the-stream + (let [ws-stream (rx/subject) + row (atom (job "running")) + seen (atom [])] + + (await + (mock/with-mocks* + {rp/cmd! (mock/stub (fn [_ _] (rx/of @row))) + ws/get-rcv-stream (mock/stub (fn [_] ws-stream))} + + (let [done (hva/observe (dj/watch-job nil job-id) + {:on-next #(swap! seen conj %)})] + + (rx/push! ws-stream + (message (event :progress {:stage :pages + :counters {:pages {:current 1 :total 2}}}))) + + (t/testing "an event of another job is not part of this stream" + (rx/push! ws-stream + (message (event (uuid/next) :progress {:stage :media}))) + (await (hva/settle)) + (t/is (= [{:kind :progress + :payload {:stage :pages + :counters {:pages {:current 1 :total 2}}}}] + @seen))) + + (t/testing "the closing event makes the stream read the row and end" + (reset! row (job "completed" :result {:resource-uri "uri"})) + (rx/push! ws-stream (message (event :end {:outcome "completed"}))) + (await done) + + (t/is (= [{:kind :progress + :payload {:stage :pages + :counters {:pages {:current 1 :total 2}}}} + {:status "completed" :result {:resource-uri "uri"}}] + @seen)))))))) + +(t/deftest ^:async a-job-that-already-ended-needs-no-event + (let [ws-stream (rx/subject) + seen (atom [])] + + (await + (mock/with-mocks* + {rp/cmd! (mock/stub (fn [_ _] + (rx/of (job "completed" :result {:resource-uri "uri"})))) + ws/get-rcv-stream (mock/stub (fn [_] ws-stream))} + + (let [done (hva/observe (dj/watch-job nil job-id) + {:on-next #(swap! seen conj %)})] + (await done) + + (t/testing "the first read of the row is the outcome" + (t/is (= [{:status "completed" :result {:resource-uri "uri"}}] + @seen)))))))) + +(t/deftest ^:async a-reconnect-reads-the-row-again + (let [ws-stream (rx/subject) + row (atom (job "running")) + seen (atom [])] + + (await + (mock/with-mocks* + {rp/cmd! (mock/stub (fn [_ _] (rx/of @row))) + ws/get-rcv-stream (mock/stub (fn [_] ws-stream))} + + (let [done (hva/observe (dj/watch-job nil job-id) + {:on-next #(swap! seen conj %)})] + (await (hva/settle)) + + (t/testing "a job that is still running reports nothing" + (t/is (= [] @seen))) + + (t/testing "a reconnect answers for a job that failed while away" + (reset! row (job "failed" :error {:type :internal + :code :boom + :hint "boom"})) + (rx/push! ws-stream {:type :opened}) + (await done) + + (t/is (= [{:status "failed" + :error {:type :internal :code :boom :hint "boom"}}] + @seen)))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; THE COMMANDS THAT FEED IT +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- capture-requests! + [requests] + (fn [request] + (swap! requests conj request) + (rx/of {:status 200 + :uri (:uri request) + :headers {"content-type" "application/json"} + :body "{}"}))) + +(t/deftest ^:async the-commands-point-at-the-jobs-api + (let [requests (atom [])] + + (await + (mock/with-mocks* + {http/fetch (mock/stub (capture-requests! requests)) + http/response->map (mock/stub identity) + http/process-response-type (mock/stub (fn [_ response] (rx/of response)))} + + (await (hva/->promise (rp/cmd! :create-export-binfile-job + {:file-ids #{job-id} :export-type :detach-libraries}))) + (await (hva/->promise (rp/cmd! :create-import-binfile-job {:project-id (str job-id)}))) + (await (hva/->promise (rp/cmd! :get-job {:id (str job-id)}))) + (await (hva/->promise (rp/cmd! :create-export-job {}))) + + (let [[export import read exporter] @requests] + (t/testing "the binfile export asks the backend, not the exporter service" + (t/is (= :post (:method export))) + (t/is (str/ends-with? (:path (:uri export)) + "/api/main/methods/create-export-binfile-job"))) + + (t/testing "the import job needs no override: its name is the method" + (t/is (= :post (:method import))) + (t/is (str/ends-with? (:path (:uri import)) + "/api/main/methods/create-import-binfile-job"))) + + (t/testing "the read is a get with the id of the job" + (t/is (= :get (:method read))) + (t/is (str/ends-with? (:path (:uri read)) + "/api/main/methods/get-job")) + (t/is (= (str job-id) (:id (:query read))))) + + (t/testing "and the exporter keeps its own name" + (t/is (= :post (:method exporter))) + (t/is (str/ends-with? (:path (:uri exporter)) "/api/export/jobs")))))))) diff --git a/frontend/test/frontend_tests/data/uploads_test.cljs b/frontend/test/frontend_tests/data/uploads_test.cljs index 0a7806b5cc..5dcec770e4 100644 --- a/frontend/test/frontend_tests/data/uploads_test.cljs +++ b/frontend/test/frontend_tests/data/uploads_test.cljs @@ -13,6 +13,7 @@ [app.main.data.uploads :as uploads] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true] + [frontend-tests.helpers.async :as hva] [frontend-tests.helpers.http :as http])) ;; --------------------------------------------------------------------------- @@ -115,3 +116,42 @@ (http/restore-fetch! orig) (t/is (= 3 @chunk-calls)) (done)))))))) + +(t/deftest ^:async upload-blob-chunked-reports-progress-per-chunk + (let [session-id (uuid/next) + ;; Three chunks of 10 bytes: 2 full + 1 partial + blob (make-blob 25) + seen (atom []) + + fetch-mock + (fn [url _opts] + (let [cmd (http/url->cmd url)] + (js/Promise.resolve + (case cmd + :create-upload-session + (http/make-transit-response + {:session-id session-id}) + + :upload-chunk + (http/make-transit-response + {:session-id session-id :index 0}) + + (http/make-json-response + {:error (str "unexpected cmd: " cmd)}))))) + + orig (http/install-fetch-mock! fetch-mock)] + + (await + (hva/observe (uploads/upload-blob-chunked blob + :chunk-size 10 + :on-progress #(swap! seen conj %)) + {:on-next (fn [{:keys [session-id]}] + (t/is (uuid? session-id)))})) + (http/restore-fetch! orig) + + (t/testing "every uploaded chunk is reported with uploaded/total" + ;; chunks upload one at a time, so they are reported in order + (t/is (= [{:current 1 :total 3} + {:current 2 :total 3} + {:current 3 :total 3}] + @seen))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 501973b6e8..a12c63a205 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -12,6 +12,9 @@ [frontend-tests.data.dashboard-test] [frontend-tests.data.event-test] [frontend-tests.data.exports-assets-test] + [frontend-tests.data.exports-files-test] + [frontend-tests.data.imports-test] + [frontend-tests.data.jobs-test] [frontend-tests.data.nitrate-test] [frontend-tests.data.persistence-retry-test] [frontend-tests.data.persistence-test] @@ -112,6 +115,7 @@ [frontend-tests.ui.history-test] [frontend-tests.ui.inspect-geometry-test] [frontend-tests.ui.inspect-stroke-tokens-test] + [frontend-tests.ui.jobs-progress-test] [frontend-tests.ui.layout-container-multiple-test] [frontend-tests.ui.measurements-test] [frontend-tests.ui.measures-menu-props-test] @@ -162,6 +166,9 @@ 'frontend-tests.data.repo-test 'frontend-tests.data.store-test 'frontend-tests.data.exports-assets-test + 'frontend-tests.data.exports-files-test + 'frontend-tests.data.imports-test + 'frontend-tests.data.jobs-test 'frontend-tests.data.svg-upload-test 'frontend-tests.data.uploads-test 'frontend-tests.data.viewer-test @@ -254,6 +261,7 @@ 'frontend-tests.ui.history-test 'frontend-tests.ui.inspect-geometry-test 'frontend-tests.ui.inspect-stroke-tokens-test + 'frontend-tests.ui.jobs-progress-test 'frontend-tests.ui.layout-container-multiple-test 'frontend-tests.ui.measurements-test 'frontend-tests.ui.measures-menu-props-test diff --git a/frontend/test/frontend_tests/ui/jobs_progress_test.cljs b/frontend/test/frontend_tests/ui/jobs_progress_test.cljs new file mode 100644 index 0000000000..9d25f852af --- /dev/null +++ b/frontend/test/frontend_tests/ui/jobs_progress_test.cljs @@ -0,0 +1,44 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.ui.jobs-progress-test + "How a job milestone is written: the stage and its counter, and the + upload stage as a percentage of the file." + (:require + [app.main.ui.jobs.progress :as jp] + [app.util.i18n :as i18n] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.mock :as mock])) + +(def ^:private stub-tr + (mock/stub (fn [k] (str "TR:" k)))) + +(t/deftest a-counter-milestone-writes-its-stage-and-counter + (with-redefs [i18n/tr stub-tr] + (t/is (= "TR:jobs.progress.stage.pages 3/8" + (jp/milestone-text {:stage :pages :counters {:pages {:current 3 :total 8}}}))) + (t/testing "a counter with no total writes the bare current" + (t/is (= "TR:jobs.progress.stage.pages 3" + (jp/milestone-text {:stage :pages :counters {:pages {:current 3}}})))))) + +(t/deftest a-file-milestone-appends-the-file-counter + (with-redefs [i18n/tr stub-tr] + (t/is (= "TR:jobs.progress.stage.pages 3/8 · TR:jobs.progress.stage.files 1/2" + (jp/milestone-text {:stage :pages + :counters {:pages {:current 3 :total 8} + :files {:current 1 :total 2}}} + :file? true))))) + +(t/deftest an-upload-milestone-writes-the-percentage-of-the-file + (with-redefs [i18n/tr stub-tr] + (t/testing "the chunks sent become a whole percentage" + (t/is (= "TR:jobs.progress.stage.upload (33%)" + (jp/milestone-text {:stage :upload :counters {:upload {:current 1 :total 3}}}))) + (t/is (= "TR:jobs.progress.stage.upload (100%)" + (jp/milestone-text {:stage :upload :counters {:upload {:current 9 :total 9}}})))) + (t/testing "an upload with no total keeps the bare label" + (t/is (= "TR:jobs.progress.stage.upload" + (jp/milestone-text {:stage :upload :counters {:upload {:current 1}}})))))) diff --git a/frontend/test/frontend_tests/util_zip_test.cljs b/frontend/test/frontend_tests/util_zip_test.cljs index a9fbc658f7..6621335a8d 100644 --- a/frontend/test/frontend_tests/util_zip_test.cljs +++ b/frontend/test/frontend_tests/util_zip_test.cljs @@ -7,38 +7,9 @@ (ns frontend-tests.util-zip-test (:require [app.util.zip :as-alias uz] - [app.worker.import :as worker.import] - [beicon.v2.core :as rx] - [cljs.test :as t :include-macros true] - [frontend-tests.helpers.mock :as mock] - [promesa.core :as p])) + [cljs.test :as t :include-macros true])) (t/deftest read-as-text-nil-entry-raises-typed-error (t/testing "read-as-text guards against nil entry" (t/is (thrown-with-msg? js/Error #"nil" (app.util.zip/read-as-text nil))))) - -(t/deftest read-zip-manifest-missing-throws-validation-error - (t/async - done - (t/testing "read-zip-manifest rejects ZIPs without manifest.json" - (mock/with-mocks - {app.util.zip/get-entry (mock/stub (fn [_ _] (p/resolved nil)))} - (fn [done'] - (->> (worker.import/read-zip-manifest #js {}) - (rx/subs! - (fn [_] - (t/is false "expected validation error to be thrown") - (done')) - (fn [err] - (let [data (ex-data err)] - (t/is (= :invalid-penpot-file (:code data)) - "missing manifest.json raises typed :invalid-penpot-file error") - (t/is (string? (:hint data)) - "missing manifest.json error carries a :hint") - (t/is (re-find #"manifest\.json" (:hint data)) - "missing manifest.json :hint mentions manifest.json") - (t/is (nil? (re-find #"getData" (:hint data))) - "missing manifest.json :hint does not leak the raw TypeError text") - (done')))))) - done)))) \ No newline at end of file diff --git a/frontend/translations/en.po b/frontend/translations/en.po index c38fce16ae..d71621ab02 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -715,34 +715,6 @@ msgstr "Next file" msgid "dashboard.import.no-perms" msgstr "You don’t have permission to import to this team" -#: src/app/main/ui/dashboard/import.cljs:134 -msgid "dashboard.import.progress.process-colors" -msgstr "Processing colors" - -#: src/app/main/ui/dashboard/import.cljs:143, src/app/main/ui/dashboard/import.cljs:146 -msgid "dashboard.import.progress.process-components" -msgstr "Processing components" - -#: src/app/main/ui/dashboard/import.cljs:140 -msgid "dashboard.import.progress.process-media" -msgstr "Processing media" - -#: src/app/main/ui/dashboard/import.cljs:131 -msgid "dashboard.import.progress.process-page" -msgstr "Processing page: %s" - -#: src/app/main/ui/dashboard/import.cljs:137 -msgid "dashboard.import.progress.process-typographies" -msgstr "Processing typographies" - -#: src/app/main/ui/dashboard/import.cljs:125 -msgid "dashboard.import.progress.upload-data" -msgstr "Uploading data to server (%s/%s)" - -#: src/app/main/ui/dashboard/import.cljs:128 -msgid "dashboard.import.progress.upload-media" -msgstr "Uploading file: %s" - #: src/app/main/ui/dashboard/import.cljs:443 msgid "dashboard.import.resolve-libraries" msgstr "" @@ -2193,18 +2165,6 @@ msgstr "Size and position" msgid "inspect.attributes.stroke" msgstr "Stroke" -#, permanent, unused -msgid "inspect.attributes.stroke.alignment.center" -msgstr "Center" - -#, permanent, unused -msgid "inspect.attributes.stroke.alignment.inner" -msgstr "Inside" - -#, permanent, unused -msgid "inspect.attributes.stroke.alignment.outer" -msgstr "Outside" - #: src/app/main/ui/inspect/attributes/text.cljs:53, src/app/main/ui/inspect/attributes/text.cljs:158 msgid "inspect.attributes.typography" msgstr "Typography" @@ -2614,6 +2574,74 @@ msgstr "The token will expire on %s" msgid "integrations.token.will-not-expire" msgstr "The token has no expiration date" +#: src/app/main/ui/exports/files.cljs:111 +msgid "jobs.export-cancelled" +msgstr "Export cancelled" + +#: src/app/main/data/imports.cljs:259 +msgid "jobs.import-cancelled" +msgstr "Import cancelled" + +#: src/app/main/ui/jobs/progress.cljs:26 +msgid "jobs.progress.stage.colors" +msgstr "Processing the colors" + +#: src/app/main/ui/jobs/progress.cljs:25 +msgid "jobs.progress.stage.components" +msgstr "Processing the components" + +#: src/app/main/ui/jobs/progress.cljs:21 +msgid "jobs.progress.stage.files" +msgstr "Processing the files" + +#: src/app/main/ui/jobs/progress.cljs:32 +msgid "jobs.progress.stage.manifest" +msgstr "Reading the package" + +#: src/app/main/ui/jobs/progress.cljs:23 +msgid "jobs.progress.stage.media" +msgstr "Processing the media" + +#: src/app/main/ui/jobs/progress.cljs:31 +msgid "jobs.progress.stage.metadata" +msgstr "Reading the metadata" + +#: src/app/main/ui/jobs/progress.cljs:22 +msgid "jobs.progress.stage.pages" +msgstr "Processing the pages" + +#: src/app/main/ui/jobs/progress.cljs:30 +msgid "jobs.progress.stage.relations" +msgstr "Processing the library relations" + +#: src/app/main/ui/jobs/progress.cljs:29 +msgid "jobs.progress.stage.storage-objects" +msgstr "Processing the storage objects" + +#: src/app/main/ui/jobs/progress.cljs:24 +msgid "jobs.progress.stage.thumbnails" +msgstr "Processing the thumbnails" + +#: src/app/main/ui/jobs/progress.cljs:28 +msgid "jobs.progress.stage.tokens" +msgstr "Processing the tokens" + +#: src/app/main/ui/jobs/progress.cljs:27 +msgid "jobs.progress.stage.typographies" +msgstr "Processing the typographies" + +#: src/app/main/ui/jobs/progress.cljs:33 +msgid "jobs.progress.stage.unknown" +msgstr "Working" + +#: src/app/main/ui/jobs/progress.cljs:33 +msgid "jobs.progress.stage.upload" +msgstr "Uploading file…" + +#: src/app/main/ui/dashboard/import.cljs:328, src/app/main/ui/exports/files.cljs:103 +msgid "jobs.queued" +msgstr "Queued" + #: src/app/main/ui/dashboard/comments.cljs:94 msgid "label.mark-all-as-read" msgstr "Mark all as read" @@ -2844,10 +2872,6 @@ msgstr "Download %s" msgid "labels.download-simple" msgstr "Download" -#: src/app/main/ui/exports/files.cljs:217 -msgid "labels.downloading-file" -msgstr "Downloading file…" - #: src/app/main/ui/dashboard/file_menu.cljs:28, src/app/main/ui/dashboard/files.cljs:86, src/app/main/ui/dashboard/files.cljs:207, src/app/main/ui/dashboard/projects.cljs:225, src/app/main/ui/dashboard/projects.cljs:228, src/app/main/ui/dashboard/sidebar.cljs:362 msgid "labels.drafts" msgstr "Drafts" @@ -3439,10 +3463,6 @@ msgstr "Upload custom fonts" msgid "labels.uploading" msgstr "Uploading…" -#: src/app/main/ui/dashboard/import.cljs:648 -msgid "labels.uploading-file" -msgstr "Uploading file…" - #: src/app/main/ui/inspect/right_sidebar.cljs:65, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1034 msgid "labels.variant" msgstr "Variant" @@ -6251,10 +6271,6 @@ msgstr "Typographies" msgid "workspace.assets.typography.add-typography" msgstr "Add typography" -#: src/app/main/ui/workspace/sidebar/assets/typographies.cljs:461 -msgid "workspace.assets.typography.multiple-texts-selected" -msgstr "Select one text to create a typography style" - #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:855 msgid "workspace.assets.typography.font-size" msgstr "Size" @@ -6275,6 +6291,10 @@ msgstr "Letter Spacing" msgid "workspace.assets.typography.line-height" msgstr "Line Height" +#: src/app/main/ui/workspace/sidebar/assets/typographies.cljs:461 +msgid "workspace.assets.typography.multiple-texts-selected" +msgstr "Select one text to create a typography style" + #: src/app/main/ui/dashboard/grid.cljs:226, src/app/main/ui/workspace/libraries.cljs:1070, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:802, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:838, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:957, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:976 msgid "workspace.assets.typography.sample" msgstr "Ag" @@ -9214,10 +9234,6 @@ msgstr "Line (%s)" msgid "workspace.toolbar.mcp" msgstr "MCP" -#: src/app/main/ui/workspace/top_toolbar.cljs:345 -msgid "workspace.toolbar.mcp-connect-here" -msgstr "Connect here" - #: src/app/main/ui/workspace/top_toolbar.cljs:342 msgid "workspace.toolbar.mcp-connected" msgstr "MCP connected" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 00ea23e0b5..c1464fe980 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -726,34 +726,6 @@ msgstr "Siguiente archivo" msgid "dashboard.import.no-perms" msgstr "No tienes permisos para importar en este equipo" -#: src/app/main/ui/dashboard/import.cljs:134 -msgid "dashboard.import.progress.process-colors" -msgstr "Procesando colores" - -#: src/app/main/ui/dashboard/import.cljs:143, src/app/main/ui/dashboard/import.cljs:146 -msgid "dashboard.import.progress.process-components" -msgstr "Procesando componentes" - -#: src/app/main/ui/dashboard/import.cljs:140 -msgid "dashboard.import.progress.process-media" -msgstr "Procesando media" - -#: src/app/main/ui/dashboard/import.cljs:131 -msgid "dashboard.import.progress.process-page" -msgstr "Procesando página: %s" - -#: src/app/main/ui/dashboard/import.cljs:137 -msgid "dashboard.import.progress.process-typographies" -msgstr "Procesando tipografías" - -#: src/app/main/ui/dashboard/import.cljs:125 -msgid "dashboard.import.progress.upload-data" -msgstr "Enviando datos al servidor (%s/%s)" - -#: src/app/main/ui/dashboard/import.cljs:128 -msgid "dashboard.import.progress.upload-media" -msgstr "Enviando fichero: %s" - #: src/app/main/ui/dashboard/import.cljs:443 msgid "dashboard.import.resolve-libraries" msgstr "" @@ -1542,6 +1514,12 @@ msgstr "" msgid "errors.comment-error" msgstr "Ha habido un error con el comentario" +#: src/app/main/errors.cljs:499 +msgid "errors.connection-error" +msgstr "" +"No se puede conectar con el servidor. Comprueba tu conexión e inténtalo de " +"nuevo." + #: src/app/main/errors.cljs:851 msgid "errors.deprecated" msgstr "" @@ -1782,6 +1760,10 @@ msgstr "" "informar del error y recarga para continuar desde los últimos cambios " "guardados." +#: src/app/main/data/persistence.cljs:307 +msgid "errors.save-retrying" +msgstr "Se ha perdido la conexión. Reintentando guardar los cambios." + #: src/app/main/errors.cljs:773 msgid "errors.svg-parser.invalid-svg" msgstr "El SVG no es válido o está mal formado" @@ -2224,18 +2206,6 @@ msgstr "Tamaño y posición" msgid "inspect.attributes.stroke" msgstr "Borde" -#, permanent, unused -msgid "inspect.attributes.stroke.alignment.center" -msgstr "Centro" - -#, permanent, unused -msgid "inspect.attributes.stroke.alignment.inner" -msgstr "Interior" - -#, permanent, unused -msgid "inspect.attributes.stroke.alignment.outer" -msgstr "Exterior" - #: src/app/main/ui/inspect/attributes/text.cljs:53, src/app/main/ui/inspect/attributes/text.cljs:158 msgid "inspect.attributes.typography" msgstr "Tipografía" @@ -2647,6 +2617,74 @@ msgstr "El token expirará el %s" msgid "integrations.token.will-not-expire" msgstr "El token no tiene fecha de expiración" +#: src/app/main/ui/exports/files.cljs:111 +msgid "jobs.export-cancelled" +msgstr "Exportación cancelada" + +#: src/app/main/data/imports.cljs:259 +msgid "jobs.import-cancelled" +msgstr "Importación cancelada" + +#: src/app/main/ui/jobs/progress.cljs:26 +msgid "jobs.progress.stage.colors" +msgstr "Procesando colores" + +#: src/app/main/ui/jobs/progress.cljs:25 +msgid "jobs.progress.stage.components" +msgstr "Procesando componentes" + +#: src/app/main/ui/jobs/progress.cljs:21 +msgid "jobs.progress.stage.files" +msgstr "Procesando archivos" + +#: src/app/main/ui/jobs/progress.cljs:32 +msgid "jobs.progress.stage.manifest" +msgstr "Leyendo el paquete" + +#: src/app/main/ui/jobs/progress.cljs:23 +msgid "jobs.progress.stage.media" +msgstr "Procesando media" + +#: src/app/main/ui/jobs/progress.cljs:31 +msgid "jobs.progress.stage.metadata" +msgstr "Leyendo metadatos" + +#: src/app/main/ui/jobs/progress.cljs:22 +msgid "jobs.progress.stage.pages" +msgstr "Procesando páginas" + +#: src/app/main/ui/jobs/progress.cljs:30 +msgid "jobs.progress.stage.relations" +msgstr "Procesando relaciones de biblioteca" + +#: src/app/main/ui/jobs/progress.cljs:29 +msgid "jobs.progress.stage.storage-objects" +msgstr "Procesando objetos de almacenamiento" + +#: src/app/main/ui/jobs/progress.cljs:24 +msgid "jobs.progress.stage.thumbnails" +msgstr "Procesando miniaturas" + +#: src/app/main/ui/jobs/progress.cljs:28 +msgid "jobs.progress.stage.tokens" +msgstr "Procesando tokens" + +#: src/app/main/ui/jobs/progress.cljs:27 +msgid "jobs.progress.stage.typographies" +msgstr "Procesando tipografías" + +#: src/app/main/ui/jobs/progress.cljs:33 +msgid "jobs.progress.stage.unknown" +msgstr "Trabajando" + +#: src/app/main/ui/jobs/progress.cljs:33 +msgid "jobs.progress.stage.upload" +msgstr "Subiendo archivo…" + +#: src/app/main/ui/dashboard/import.cljs:328, src/app/main/ui/exports/files.cljs:103 +msgid "jobs.queued" +msgstr "En cola" + #: src/app/main/ui/dashboard/comments.cljs:94 msgid "label.mark-all-as-read" msgstr "Marcar todo como leído" @@ -2877,10 +2915,6 @@ msgstr "Descargar %s" msgid "labels.download-simple" msgstr "Descargar" -#: src/app/main/ui/exports/files.cljs:217 -msgid "labels.downloading-file" -msgstr "Descargando archivo…" - #: src/app/main/ui/dashboard/file_menu.cljs:28, src/app/main/ui/dashboard/files.cljs:86, src/app/main/ui/dashboard/files.cljs:207, src/app/main/ui/dashboard/projects.cljs:225, src/app/main/ui/dashboard/projects.cljs:228, src/app/main/ui/dashboard/sidebar.cljs:362 msgid "labels.drafts" msgstr "Borradores" @@ -3476,10 +3510,6 @@ msgstr "Cargar fuente" msgid "labels.uploading" msgstr "Subiendo…" -#: src/app/main/ui/dashboard/import.cljs:648 -msgid "labels.uploading-file" -msgstr "Subiendo archivo…" - #: src/app/main/ui/inspect/right_sidebar.cljs:65, src/app/main/ui/workspace/sidebar/options/menus/component.cljs:1034 msgid "labels.variant" msgstr "Variante" @@ -4986,6 +5016,14 @@ msgstr "Cancelar" msgid "shortcuts.export-shapes" msgstr "Exportar elementos" +#: src/app/main/data/workspace/shortcuts.cljs:184 +msgid "shortcuts.find" +msgstr "Buscar" + +#: src/app/main/data/workspace/shortcuts.cljs:188 +msgid "shortcuts.find-and-replace" +msgstr "Buscar y reemplazar" + #: src/app/main/data/workspace/grid_layout/shortcuts.cljs:75, src/app/main/data/workspace/path/shortcuts.cljs:226, src/app/main/data/workspace/shortcuts.cljs:711 msgid "shortcuts.fit-all" msgstr "Zoom abarcar todo" @@ -5222,6 +5260,10 @@ msgstr "Pegar" msgid "shortcuts.paste-props" msgstr "Pegar propiedades" +#: src/app/main/data/workspace/shortcuts.cljs:125 +msgid "shortcuts.paste-replace" +msgstr "Pegar y reemplazar" + #: src/app/main/ui/settings/shortcuts.cljs:296, src/app/main/ui/settings/shortcuts.cljs:523 msgid "shortcuts.personalized" msgstr "Personalizados" @@ -6320,10 +6362,6 @@ msgstr "Tipografías" msgid "workspace.assets.typography.add-typography" msgstr "Añadir tipografía" -#: src/app/main/ui/workspace/sidebar/assets/typographies.cljs:461 -msgid "workspace.assets.typography.multiple-texts-selected" -msgstr "Selecciona un solo texto para crear un estilo de tipografía" - #: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:855 msgid "workspace.assets.typography.font-size" msgstr "Tamaño" @@ -6344,6 +6382,10 @@ msgstr "Interletrado" msgid "workspace.assets.typography.line-height" msgstr "Interlineado" +#: src/app/main/ui/workspace/sidebar/assets/typographies.cljs:461 +msgid "workspace.assets.typography.multiple-texts-selected" +msgstr "Selecciona un solo texto para crear un estilo de tipografía" + #: src/app/main/ui/dashboard/grid.cljs:226, src/app/main/ui/workspace/libraries.cljs:1070, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:802, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:838, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:957, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:976 msgid "workspace.assets.typography.sample" msgstr "Ag" @@ -6670,6 +6712,10 @@ msgstr "Desbloquear guías" msgid "workspace.header.reset-zoom" msgstr "Restablecer" +#: src/app/main/ui/workspace/left_header.cljs:125 +msgid "workspace.header.retrying" +msgstr "Reintentando guardar" + #: src/app/main/ui/workspace/left_header.cljs:126 msgid "workspace.header.save-error" msgstr "Error al guardar" @@ -9161,6 +9207,10 @@ msgstr "Tamaño" msgid "workspace.tokens.source" msgstr "Fuente:" +#: src/app/main/ui/workspace/tokens/management/context_menu.cljs:311, src/app/main/ui/workspace/tokens/management/context_menu.cljs:335 +msgid "workspace.tokens.stroke-width" +msgstr "Ancho del trazo" + #: src/app/main/ui/workspace/tokens/management/forms/form_container.cljs:54, src/app/main/ui/workspace/tokens/management/forms/typography.cljs:161 msgid "workspace.tokens.text-case-value-enter" msgstr "none | uppercase | lowercase | capitalize o {alias}" @@ -9297,14 +9347,22 @@ msgstr "Línea (%s)" msgid "workspace.toolbar.mcp" msgstr "MCP" -#: src/app/main/ui/workspace/top_toolbar.cljs:345 -msgid "workspace.toolbar.mcp-connect-here" -msgstr "Conectar aquí" - #: src/app/main/ui/workspace/top_toolbar.cljs:342 msgid "workspace.toolbar.mcp-connected" msgstr "MCP conectado" +#: src/app/main/ui/workspace/top_toolbar.cljs +msgid "workspace.toolbar.mcp-copy-session-id" +msgstr "Copiar ID de sesión" + +#: src/app/main/ui/workspace/top_toolbar.cljs +msgid "workspace.toolbar.mcp-session-copied" +msgstr "ID de sesión copiado" + +#: src/app/main/ui/workspace/top_toolbar.cljs +msgid "workspace.toolbar.mcp-session-id" +msgstr "ID de sesión: %s" + #: src/app/main/ui/workspace/top_toolbar.cljs:65, src/app/main/ui/workspace/top_toolbar.cljs:425 msgid "workspace.toolbar.move" msgstr "Mover (%s)" From cc5f2f57adb78aaad3fb2ef6ac0fcc0ebdd053d7 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 7 Oct 2026 12:08:33 +0200 Subject: [PATCH 049/145] :arrow_up: Upgrade skia-safe to 0.153.3 with wasm SIMD (#12148) Point SKIA_BINARIES_URL at the 0.153.3 simd prebuild and adapt fonts, path stroke expansion, and gradients to the new API. --- render-wasm/Cargo.lock | 67 ++++++++++++++++---------- render-wasm/Cargo.toml | 2 +- render-wasm/_build_env | 2 +- render-wasm/lint | 3 +- render-wasm/src/render.rs | 3 +- render-wasm/src/render/fonts.rs | 13 +++-- render-wasm/src/render/images.rs | 5 +- render-wasm/src/render/strokes.rs | 4 +- render-wasm/src/render/ui/rulers.rs | 14 +++--- render-wasm/src/shapes/fills.rs | 33 ++++++++----- render-wasm/src/shapes/stroke_paths.rs | 7 +-- render-wasm/src/shapes/text_paths.rs | 2 +- render-wasm/test | 2 +- render-wasm/watch_test | 2 +- 14 files changed, 96 insertions(+), 63 deletions(-) diff --git a/render-wasm/Cargo.lock b/render-wasm/Cargo.lock index 05fbd20e6a..a5c139b930 100644 --- a/render-wasm/Cargo.lock +++ b/render-wasm/Cargo.lock @@ -23,6 +23,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bezier-rs" version = "0.4.0" @@ -49,7 +55,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn", + "syn 2.0.106", ] [[package]] @@ -334,7 +340,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -393,7 +399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "910d41a655dac3b764f1ade94821093d3610248694320cd072303a8eedcf221d" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.106", ] [[package]] @@ -425,9 +431,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -437,9 +443,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -448,15 +454,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "render" version = "0.1.0" dependencies = [ - "base64", + "base64 0.22.1", "bezier-rs", "gl", "glam", @@ -525,7 +531,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -563,9 +569,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "skia-bindings" -version = "0.93.1" +version = "0.153.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2359f7e30c9da3f322f8ca3d4ec0abbc12a40035ce758309db0cdab07b5d4476" +checksum = "eb325787c91b3f6c34a7c6f2b8a2297b20292d733ca53a08c233e5028aec49af" dependencies = [ "bindgen", "cc", @@ -580,11 +586,11 @@ dependencies = [ [[package]] name = "skia-safe" -version = "0.93.1" +version = "0.153.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e837ea9d531c9efee8f980bfcdb7226b21db0285b0c3171d8be745829f940" +checksum = "c61cb643dac43067f3eb76c866635a74377ac0abca0ff2590674e1e295514c3d" dependencies = [ - "base64", + "base64 0.23.1", "bitflags", "percent-encoding", "skia-bindings", @@ -593,13 +599,13 @@ dependencies = [ [[package]] name = "skia-svg-macros" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "044dd2233c9717a74f75197f3e7f0a966db2127c0ffb5e05013b480a9b75b2c7" +checksum = "fec38aa7a389d249e1ce357b050ae5bdb302fd9943661fa650f39cf6ea51be97" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] @@ -614,10 +620,21 @@ dependencies = [ ] [[package]] -name = "tar" -version = "0.4.42" +name = "syn" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -654,7 +671,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", ] [[package]] @@ -740,7 +757,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -762,7 +779,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index 8c0be81049..7b2951a2a1 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -26,7 +26,7 @@ gl = "0.14.0" glam = "0.24.2" indexmap = "2.7.1" macros = { path = "macros" } -skia-safe = { version = "0.93.1", default-features = false, features = [ +skia-safe = { version = "0.153.3", default-features = false, features = [ "gl", "svg", "textlayout", diff --git a/render-wasm/_build_env b/render-wasm/_build_env index dc03bebd89..2673c81ba4 100644 --- a/render-wasm/_build_env +++ b/render-wasm/_build_env @@ -20,7 +20,7 @@ fi export BUILD_NAME="${BUILD_NAME:-render-wasm}" export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"}; export CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-"target/$RENDER_TARGET"}; -export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp-pdf-simd.tar.gz"} +export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.153.3/skia-binaries-b7f043e0b1e2a850e702-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp-pdf-simd.tar.gz"} # 256 MB of initial heap to perform less # initial calls to memory grow. diff --git a/render-wasm/lint b/render-wasm/lint index 0489e1040d..aeacf782c4 100755 --- a/render-wasm/lint +++ b/render-wasm/lint @@ -10,8 +10,7 @@ fi . ./_build_env frontend -export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"}; -export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp-pdf-simd.tar.gz"} +export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.153.3/skia-binaries-b7f043e0b1e2a850e702-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp-pdf-simd.tar.gz"} ALLOWED_RULES="-D static_mut_refs" diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 65f7665dec..44835b88f4 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -767,8 +767,9 @@ impl RenderState { paint.set_style(skia::PaintStyle::Stroke); paint.set_stroke_width(stroke_outset * 2.0); - let mut outline = skia::Path::default(); + let mut outline = skia::PathBuilder::new(); if skia::path_utils::fill_path_with_paint(&base, &paint, &mut outline, None, None) { + let outline = outline.detach(); if let Some(united) = base.op(&outline, skia::PathOp::Union) { return united; } diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index 78ae886064..b1a09531cc 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -33,7 +33,12 @@ pub struct FontStore { impl FontStore { pub fn try_new() -> Result { - let font_mgr = FontMgr::new(); + // custom_empty: FreeType-backed manager with no system scan. FontMgr::new() + // calls SkFontMgr_NewSystem, which on wasm tries to opendir font paths and + // aborts when Emscripten is built with -sFILESYSTEM=0 (exporter + browser). + let font_mgr = FontMgr::custom_empty().ok_or(Error::CriticalError( + "Failed to create empty FontMgr".to_string(), + ))?; let font_provider = load_default_provider(&font_mgr); let mut font_collection = skia::textlayout::FontCollection::new(); font_collection.set_default_font_manager(FontMgr::from(font_provider.clone()), None); @@ -47,7 +52,7 @@ impl FontStore { let debug_font = skia::Font::new(debug_typeface, 12.0); let ui_typeface = font_mgr - .new_from_data(UI_FONT_BYTES, None) + .new_from_data(skia::Data::new_copy(UI_FONT_BYTES), None) .ok_or(Error::CriticalError("Failed to load UI font".to_string()))?; let ui_font = skia::Font::new(ui_typeface, 12.0); @@ -96,7 +101,7 @@ impl FontStore { let typeface = self .font_mgr - .new_from_data(font_data, None) + .new_from_data(skia::Data::new_copy(font_data), None) .ok_or(Error::CriticalError( "Failed to create typeface".to_string(), ))?; @@ -265,7 +270,7 @@ fn load_default_provider(font_mgr: &FontMgr) -> skia::textlayout::TypefaceFontPr let family = FontFamily::new(default_font_uuid(), 400, FontStyle::Normal); let font = font_mgr - .new_from_data(DEFAULT_FONT_BYTES, None) + .new_from_data(skia::Data::new_copy(DEFAULT_FONT_BYTES), None) .expect("Failed to load font"); font_provider.register_typeface(font, family.alias().as_str()); diff --git a/render-wasm/src/render/images.rs b/render-wasm/src/render/images.rs index 4b1ce66754..5b59b3620a 100644 --- a/render-wasm/src/render/images.rs +++ b/render-wasm/src/render/images.rs @@ -186,8 +186,9 @@ const DEFAULT_SVG_SIZE: f32 = 512.0; fn parse_svg(raw_data: &[u8]) -> Option<(skia::svg::Dom, Size)> { // An empty font manager: elements inside SVG image fills won't // resolve typefaces. Wire the render state's font provider here if that - // ever becomes a need. - let font_mgr = skia::FontMgr::new(); + // ever becomes a need. Must be custom_empty (not FontMgr::new): NewSystem + // scans font dirs and aborts under -sFILESYSTEM=0. + let font_mgr = skia::FontMgr::custom_empty()?; let mut dom = skia::svg::Dom::from_bytes(raw_data, font_mgr).ok()?; let mut size = dom.root().intrinsic_size(); diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 48efeada92..8a89015034 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -439,7 +439,7 @@ fn expand_side_patterns( line_pb.detach() }; - let mut outline = skia::Path::default(); + let mut outline = skia::PathBuilder::new(); if skia::path_utils::fill_path_with_paint( &line_path, &stamp_paint, @@ -447,7 +447,7 @@ fn expand_side_patterns( None, None, ) { - pb.add_path(&outline); + pb.add_path(&outline.detach(), None); expanded = true; } } diff --git a/render-wasm/src/render/ui/rulers.rs b/render-wasm/src/render/ui/rulers.rs index 16bf6f4acc..18e9d0a4e3 100644 --- a/render-wasm/src/render/ui/rulers.rs +++ b/render-wasm/src/render/ui/rulers.rs @@ -364,14 +364,12 @@ fn draw_mask(ctx: &RenderCtx, rect: Rect, axis: MaskAxis, fade_to_end: bool) { MaskAxis::Horizontal => (rect.right, rect.top), MaskAxis::Vertical => (rect.left, rect.bottom), }; - let shader = skia::gradient_shader::linear( - ((rect.left, rect.top), end), - &colors[..], - Some(&offsets[..]), - skia::TileMode::Clamp, - None, - None, - ); + let colors4f: Vec = colors.iter().map(|c| skia::Color4f::from(*c)).collect(); + let gradient_colors = + skia::gradient::Colors::new(&colors4f, Some(&offsets[..]), skia::TileMode::Clamp, None); + let gradient = + skia::gradient::Gradient::new(gradient_colors, skia::gradient::Interpolation::default()); + let shader = skia::shaders::linear_gradient(((rect.left, rect.top), end), &gradient, None); let mut paint = Paint::default(); paint.set_anti_alias(false); paint.set_style(PaintStyle::Fill); diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index 1faeb54185..94d33e7dd2 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -74,14 +74,20 @@ impl Gradient { rect.left + self.end.0 * rect.width(), rect.top + self.end.1 * rect.height(), ); - skia::gradient_shader::linear( - (start, end), - self.colors.as_slice(), + let colors4f: Vec = self + .colors + .iter() + .map(|c| skia::Color4f::from(*c)) + .collect(); + let colors = skia::gradient::Colors::new( + &colors4f, Some(self.offsets.as_slice()), skia::TileMode::Clamp, None, - None, - ) + ); + let gradient = + skia::gradient::Gradient::new(colors, skia::gradient::Interpolation::default()); + skia::shaders::linear_gradient((start, end), &gradient, None) } pub fn to_radial_shader(&self, rect: &Rect) -> Option { @@ -106,15 +112,20 @@ impl Gradient { transform.pre_scale((self.width * rect.width() / rect.height(), 1.), None); transform.pre_translate((-center.x, -center.y)); - skia::gradient_shader::radial( - center, - distance, - self.colors.as_slice(), + let colors4f: Vec = self + .colors + .iter() + .map(|c| skia::Color4f::from(*c)) + .collect(); + let colors = skia::gradient::Colors::new( + &colors4f, Some(self.offsets.as_slice()), skia::TileMode::Clamp, None, - Some(&transform), - ) + ); + let gradient = + skia::gradient::Gradient::new(colors, skia::gradient::Interpolation::default()); + skia::shaders::radial_gradient((center, distance), &gradient, Some(&transform)) } } diff --git a/render-wasm/src/shapes/stroke_paths.rs b/render-wasm/src/shapes/stroke_paths.rs index 52dc58e762..bcdbdbeac4 100644 --- a/render-wasm/src/shapes/stroke_paths.rs +++ b/render-wasm/src/shapes/stroke_paths.rs @@ -48,7 +48,7 @@ pub fn stroke_to_path( paint.set_stroke_cap(cap); } - let mut stroke_outline = skia::Path::default(); + let mut stroke_outline = skia::PathBuilder::new(); let success = skia::path_utils::fill_path_with_paint( &transformed_shape_path, &paint, @@ -60,6 +60,7 @@ pub fn stroke_to_path( if !success { return None; } + let stroke_outline = stroke_outline.detach(); // For inner/outer strokes, use boolean ops to clip // the 2×-width stroke outline to the correct region. @@ -198,9 +199,9 @@ fn stroke_region(path: &skia::Path, width: f32) -> Option { paint.set_style(skia::PaintStyle::Stroke); paint.set_stroke_width(width); - let mut outline = skia::Path::default(); + let mut outline = skia::PathBuilder::new(); skia::path_utils::fill_path_with_paint(path, &paint, &mut outline, None, None) - .then_some(outline) + .then(|| outline.detach()) } /// Filled geometry of a single stroke cap, matching what `handle_stroke_caps` diff --git a/render-wasm/src/shapes/text_paths.rs b/render-wasm/src/shapes/text_paths.rs index 777181a3a4..44cc21a115 100644 --- a/render-wasm/src/shapes/text_paths.rs +++ b/render-wasm/src/shapes/text_paths.rs @@ -68,7 +68,7 @@ impl TextPaths { let Some(glyph_path) = font.get_path(*glyph) else { continue; }; - builder.add_path(&glyph_path.with_offset(run_origin + *position)); + builder.add_path(&glyph_path.with_offset(run_origin + *position), None); has_glyphs = true; } diff --git a/render-wasm/test b/render-wasm/test index a94cf8025d..c27d5ea533 100755 --- a/render-wasm/test +++ b/render-wasm/test @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -x -export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"} +export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.153.3/skia-binaries-b7f043e0b1e2a850e702-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp-pdf.tar.gz"} export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"x86_64-unknown-linux-gnu"}; _SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) diff --git a/render-wasm/watch_test b/render-wasm/watch_test index 798eb84bf0..27106e49bd 100755 --- a/render-wasm/watch_test +++ b/render-wasm/watch_test @@ -1,7 +1,7 @@ #!/usr/bin/env bash _SCRIPT_DIR=$(dirname $0); -export SKIA_BINARIES_URL="https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz" +export SKIA_BINARIES_URL="https://github.com/penpot/skia-binaries/releases/download/0.153.3/skia-binaries-b7f043e0b1e2a850e702-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp-pdf.tar.gz" pushd $_SCRIPT_DIR; cargo watch -x "test --bin render_wasm -- --show-output" From 1d7c775f7207f706346b6233e49be0f0e89449c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 7 Oct 2026 12:26:45 +0200 Subject: [PATCH 050/145] :bug: Fix not setting mimetype in image fills (#12136) --- common/src/app/common/types/fills/impl.cljc | 42 ++++++++++++------- frontend/src/app/render_wasm/text_editor.cljs | 3 ++ render-wasm/src/shapes/fills.rs | 12 ++++++ render-wasm/src/wasm/fills.rs | 3 +- render-wasm/src/wasm/fills/image.rs | 5 ++- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/common/src/app/common/types/fills/impl.cljc b/common/src/app/common/types/fills/impl.cljc index 2f8ca65a6f..453b95d6b3 100644 --- a/common/src/app/common/types/fills/impl.cljc +++ b/common/src/app/common/types/fills/impl.cljc @@ -59,6 +59,27 @@ result (bit-or result n)] result)) +(defn encode-mtype + "Encode an image media type as the 16-bit code shared with render-wasm." + [mtype] + (case mtype + "image/jpeg" 0x01 + "image/png" 0x02 + "image/gif" 0x03 + "image/webp" 0x04 + "image/svg+xml" 0x05 + 0x00)) + +(defn decode-mtype + [code] + (case code + 0x01 "image/jpeg" + 0x02 "image/png" + 0x03 "image/gif" + 0x04 "image/webp" + 0x05 "image/svg+xml" + nil)) + (defn- get-color-hex [n] (let [n (bit-and n 0x00ffffff) @@ -127,12 +148,13 @@ transform (get image :transform) has-transform? (some? transform) transform-flag (if has-transform? 0x02 0x00) - flags (bit-or keep-aspect-ratio transform-flag)] + flags (bit-or keep-aspect-ratio transform-flag) + mtype (encode-mtype (get image :mtype))] (buf/write-byte buffer (+ offset 0) 0x03) (buf/write-uuid buffer (+ offset 4) image-id) (buf/write-byte buffer (+ offset 20) alpha) (buf/write-byte buffer (+ offset 21) flags) - (buf/write-short buffer (+ offset 22) 0) ;; 2-byte padding (reserved for future use) + (buf/write-short buffer (+ offset 22) mtype) (buf/write-int buffer (+ offset 24) image-width) (buf/write-int buffer (+ offset 28) image-height) (if has-transform? @@ -155,13 +177,7 @@ mtype (dm/get-in fill [:fill-image :mtype])] (when mtype - (let [val (case mtype - "image/jpeg" 0x01 - "image/png" 0x02 - "image/gif" 0x03 - "image/webp" 0x04 - "image/svg+xml" 0x05)] - (buf/write-short buffer (+ offset 2) val))) + (buf/write-short buffer (+ offset 2) (encode-mtype mtype))) (if (and (some? ref-file) (some? ref-id)) @@ -235,13 +251,7 @@ :y (buf/read-float dbuffer (+ doffset 36)) :width (buf/read-float dbuffer (+ doffset 40)) :height (buf/read-float dbuffer (+ doffset 44))}) - mtype (buf/read-short mbuffer (+ moffset 2)) - mtype (case mtype - 0x01 "image/jpeg" - 0x02 "image/png" - 0x03 "image/gif" - 0x04 "image/webp" - 0x05 "image/svg+xml")] + mtype (decode-mtype (buf/read-short mbuffer (+ moffset 2)))] {:fill-opacity opacity :fill-image (cond-> {:id id :width width diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index ed82300c09..6493128aba 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -118,12 +118,15 @@ d (aget heap-u32 (+ fill-u32-offset 4)) alpha (aget heap-u8 (+ fill-byte-offset 20)) flags (aget heap-u8 (+ fill-byte-offset 21)) + mtype (bit-or (aget heap-u8 (+ fill-byte-offset 22)) + (bit-shift-left (aget heap-u8 (+ fill-byte-offset 23)) 8)) width (aget heap-i32 (+ fill-u32-offset 6)) height (aget heap-i32 (+ fill-u32-offset 7))] {:fill-opacity (u8->opacity alpha) :fill-image {:id (uuid/from-unsigned-parts a b c d) :width width :height height + :mtype (types.fills.impl/decode-mtype mtype) :keep-aspect-ratio (not (zero? (bit-and flags 0x01))) :name "sample"}}) diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index 94d33e7dd2..c819579de0 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -145,6 +145,8 @@ pub struct ImageFill { height: i32, keep_aspect_ratio: bool, transform: Option, + // Opaque media type code, kept so the frontend can read it back. + mtype: u16, } impl ImageFill { @@ -156,6 +158,7 @@ impl ImageFill { height, keep_aspect_ratio, transform: None, + mtype: 0, } } @@ -174,9 +177,14 @@ impl ImageFill { height, keep_aspect_ratio, transform, + mtype: 0, } } + pub fn with_mtype(self, mtype: u16) -> Self { + Self { mtype, ..self } + } + pub fn id(&self) -> Uuid { self.id } @@ -197,6 +205,10 @@ impl ImageFill { self.height } + pub fn mtype(&self) -> u16 { + self.mtype + } + pub fn transform(&self) -> Option<&ImageFillTransform> { self.transform.as_ref() } diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index 5d2a6e29e2..28ff005bec 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -179,7 +179,8 @@ mod tests { #[test] fn test_image_fill_round_trip() { - let image_fill = shapes::ImageFill::new(crate::uuid::Uuid::nil(), 0x80, 300, 200, true); + let image_fill = + shapes::ImageFill::new(crate::uuid::Uuid::nil(), 0x80, 300, 200, true).with_mtype(0x02); let fill = shapes::Fill::Image(image_fill); let raw_fill = RawFillData::try_from(&fill).expect("image fill must be serializable"); let bytes = <[u8; RAW_FILL_DATA_SIZE]>::from(raw_fill); diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index e6b2e634c5..86c9a12abe 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -44,7 +44,7 @@ pub struct RawImageFillData { d: u32, opacity: u8, flags: u8, - _pad: u16, + mtype: u16, width: i32, height: i32, transform_x: f32, @@ -76,7 +76,7 @@ impl From<&ImageFill> for RawImageFillData { d, opacity: image_fill.opacity(), flags, - _pad: 0, + mtype: image_fill.mtype(), width: image_fill.width(), height: image_fill.height(), transform_x: tx, @@ -110,6 +110,7 @@ impl From for ImageFill { keep_aspect_ratio, transform, ) + .with_mtype(value.mtype) } } From 0d8bcf963742607f1873e5208cebd51e30fe9f0b Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Wed, 7 Oct 2026 12:32:14 +0200 Subject: [PATCH 051/145] :bug: Fix crash when drawing (#12101) --- .../main/ui/workspace/shapes/path/editor.cljs | 5 +++- .../main/ui/workspace/viewport/drawarea.cljs | 28 +++++++++++-------- frontend/test/frontend_tests/runner.cljs | 2 ++ .../test/frontend_tests/ui/drawarea_test.cljs | 28 +++++++++++++++++++ 4 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 frontend/test/frontend_tests/ui/drawarea_test.cljs diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs index e716223909..d49422542b 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -36,6 +36,9 @@ (def path-preview-dasharray 4) (def path-snap-stroke-width 1) +;; Stand-in for shapes that reach the editor without content. +(def ^:private empty-content (path/content nil)) + (def accent-color "var(--color-accent-tertiary)") (def secondary-color "var(--color-accent-quaternary)") (def black-color "var(--app-black)") @@ -502,7 +505,7 @@ (some? drag-handler)) base-content - (get shape :content) + (or (get shape :content) empty-content) ;; Cache segment midpoints used by insertion previews. insertion-mid-points diff --git a/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs b/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs index 910688658f..b24cc5e8a5 100644 --- a/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/drawarea.cljs @@ -35,6 +35,17 @@ (update fill :fill-opacity #(* (or % 1) edit-fill-opacity))) fills)) +(defn draw-area-kind + "Returns the overlay to render for the drawing object `shape`: + `:path`, `:curve`, `:generic` or nil. Non-path objects always get the + generic overlay, because the active tool can change while a box + drawing is still in progress." + [tool shape] + (cond + (not= :path (dm/get-prop shape :type)) :generic + (= tool :path) :path + (= tool :curve) :curve)) + (defn path-edit-shape "Builds the path shape rendered during editing." [drawing-obj stored] @@ -98,15 +109,8 @@ [:g {:style {:pointer-events "none"}} [:& shapes/shape-wrapper {:shape render-shape}]] - (cond - (= tool :path) - [:> path-draw-area* props] - - (= tool :curve) - [:& path-shape {:shape shape :zoom zoom}] - - (= (:type shape) :path) - nil - - :else - [:> generic-draw-area* props])])) + (case (draw-area-kind tool shape) + :path [:> path-draw-area* props] + :curve [:& path-shape {:shape shape :zoom zoom}] + :generic [:> generic-draw-area* props] + nil)])) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 7a7da64b1a..598e9faac4 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -103,6 +103,7 @@ [frontend-tests.ui.comment-input-ime-test] [frontend-tests.ui.comments-clustering-test] [frontend-tests.ui.comments-position-modifier-test] + [frontend-tests.ui.drawarea-test] [frontend-tests.ui.ds-controls-numeric-input-test] [frontend-tests.ui.gradient-handlers-test] [frontend-tests.ui.history-test] @@ -238,6 +239,7 @@ 'frontend-tests.ui.comment-input-ime-test 'frontend-tests.ui.comments-clustering-test 'frontend-tests.ui.comments-position-modifier-test + 'frontend-tests.ui.drawarea-test 'frontend-tests.ui.ds-controls-numeric-input-test 'frontend-tests.ui.gradient-handlers-test 'frontend-tests.ui.history-test diff --git a/frontend/test/frontend_tests/ui/drawarea_test.cljs b/frontend/test/frontend_tests/ui/drawarea_test.cljs new file mode 100644 index 0000000000..7bb715fc8f --- /dev/null +++ b/frontend/test/frontend_tests/ui/drawarea_test.cljs @@ -0,0 +1,28 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.ui.drawarea-test + (:require + [app.common.types.shape :as cts] + [app.main.ui.workspace.viewport.drawarea :as drawarea] + [cljs.test :as t :include-macros true])) + +(def ^:private box-types + [:rect :frame :circle :text]) + +(t/deftest box-drawings-get-the-generic-overlay-for-any-tool + (doseq [type box-types + tool [:path :curve type nil]] + (let [shape (cts/setup-shape {:type type :x 10 :y 10 :width 0.01 :height 0.01})] + (t/is (= :generic (drawarea/draw-area-kind tool shape)) + (str "type " type " with tool " tool))))) + +(t/deftest path-drawings-follow-the-active-tool + (let [shape (cts/setup-shape {:type :path})] + (t/is (= :path (drawarea/draw-area-kind :path shape))) + (t/is (= :curve (drawarea/draw-area-kind :curve shape))) + (t/is (nil? (drawarea/draw-area-kind :line shape))) + (t/is (nil? (drawarea/draw-area-kind nil shape))))) From edd500302ce799b0519a8b5577a32eb62f4b52e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 7 Oct 2026 11:07:22 +0200 Subject: [PATCH 052/145] :arrow_up: Update ImageMagick to 7.1.2-32 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Barragán Merino --- docker/devenv/Dockerfile | 2 +- docker/imagemagick/Dockerfile | 2 +- docker/images/Dockerfile.backend | 2 +- docker/images/Dockerfile.exporter | 2 +- manage.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 995284048a..bd329d61f1 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -550,7 +550,7 @@ ENV LANG='C.UTF-8' \ SERENA_CONTEXT="claude-code" \ PATH="/opt/jdk/bin:/opt/gh/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH" -COPY --from=penpotapp/imagemagick:7.1.2-27-1 /opt/imagick /opt/imagick +COPY --from=penpotapp/imagemagick:7.1.2-32 /opt/imagick /opt/imagick COPY --from=setup-jvm /opt/jdk /opt/jdk COPY --from=setup-jvm /opt/clojure /opt/clojure COPY --from=setup-node /opt/node /opt/node diff --git a/docker/imagemagick/Dockerfile b/docker/imagemagick/Dockerfile index 157897ca6e..bfb5544c7d 100644 --- a/docker/imagemagick/Dockerfile +++ b/docker/imagemagick/Dockerfile @@ -6,7 +6,7 @@ ENV LANG='C.UTF-8' \ DEBIAN_FRONTEND=noninteractive \ TZ=Etc/UTC -ARG IMAGEMAGICK_VERSION=7.1.2-27 +ARG IMAGEMAGICK_VERSION=7.1.2-32 RUN set -e; \ apt-get -qq update; \ diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend index bf4a33b5cf..4937b04cda 100644 --- a/docker/images/Dockerfile.backend +++ b/docker/images/Dockerfile.backend @@ -106,7 +106,7 @@ RUN set -ex; \ chown -R penpot:penpot /opt/data; COPY --from=build /opt/jre /opt/jre -COPY --from=penpotapp/imagemagick:7.1.2-27-1 /opt/imagick /opt/imagick +COPY --from=penpotapp/imagemagick:7.1.2-32 /opt/imagick /opt/imagick COPY files/imagemagick-policy.xml /opt/imagick/etc/ImageMagick-7/policy.xml diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 8d41a57273..03d468e72e 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -88,7 +88,7 @@ RUN set -ex; \ ARG BUNDLE_PATH="./bundle-exporter/" COPY --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/exporter/ -COPY --from=penpotapp/imagemagick:7.1.2-27-1 /opt/imagick /opt/imagick +COPY --from=penpotapp/imagemagick:7.1.2-32 /opt/imagick /opt/imagick WORKDIR /opt/penpot/exporter diff --git a/manage.sh b/manage.sh index 408792d781..f9a5824bfe 100755 --- a/manage.sh +++ b/manage.sh @@ -69,7 +69,7 @@ PENPOT_PORT_BASE_STORYBOOK=${PENPOT_STORYBOOK_PORT:?missing in defaults.env} export CURRENT_USER_ID=$(id -u); export CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD); -export IMAGEMAGICK_VERSION=7.1.2-27 +export IMAGEMAGICK_VERSION=7.1.2-32 # Safe directory to avoid ownership errors with Git git config --global --add safe.directory /home/penpot/penpot || true From 29072e4c6b7e9a0e485f52ff3826b9387ca5aecc Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Wed, 7 Oct 2026 13:07:07 +0200 Subject: [PATCH 053/145] :bug: Fix path editor crash on shortcut (#11996) --- .../app/main/data/workspace/path/drawing.cljs | 42 ++++++---- .../main/data/workspace/path/shortcuts.cljs | 1 + .../logic/path_lifecycle_test.cljs | 77 +++++++++++++++---- 3 files changed, 89 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index f2a0ab1377..6a2fe3a174 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -37,13 +37,16 @@ (declare change-edit-mode) (defn start-created-path-edition - [id] - (ptk/reify ::start-created-path-edition - ptk/WatchEvent - (watch [_ _ _] - (rx/of (dwe/start-edition-mode id) - (edition/start-path-edit id) - (change-edit-mode :draw))))) + "Edits a just created path in `edit-mode` (`:draw` by default)." + ([id] + (start-created-path-edition id :draw)) + ([id edit-mode] + (ptk/reify ::start-created-path-edition + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dwe/start-edition-mode id) + (edition/start-path-edit id) + (change-edit-mode edit-mode)))))) ;; Draw-loop stop signals either restart the same path or exit drawing. @@ -434,11 +437,12 @@ (when-let [content (dm/get-in state [:workspace-drawing :object :content])] (cond (and (> (count content) 1) restart?) - (rx/of (common/finish-path) - (clean-drawn-content) - (setup-frame) - (dwdc/handle-finish-drawing) - (start-created-path-edition shape-id)) + (let [edit-mode (dm/get-in state [:workspace-local :edit-path shape-id :edit-mode])] + (rx/of (common/finish-path) + (clean-drawn-content) + (setup-frame) + (dwdc/handle-finish-drawing) + (start-created-path-edition shape-id (d/nilv edit-mode :draw)))) (> (count content) 1) (rx/of (clean-drawn-content) @@ -545,6 +549,10 @@ state))) (defn change-edit-mode + "Switches the active path between `:draw` and `:move`. + + A new path being drawn has no edition yet; switching it to `:move` + finishes the drawing, which then reenters edition in `:move`." [mode] (ptk/reify ::change-edit-mode ptk/UpdateEvent @@ -552,16 +560,20 @@ (if-let [id (dm/get-in state [:workspace-local :edition])] (cond-> (d/update-in-when state [:workspace-local :edit-path id] assoc :edit-mode mode) (= mode :draw) (enter-draw-from-selected-node id)) - state)) + (cond-> state + (and (= mode :move) (st/drawing? state)) + (assoc-in [:workspace-local :edit-path (st/get-path-id state) :edit-mode] :move)))) ptk/WatchEvent (watch [_ state _] - (when-let [id (dm/get-in state [:workspace-local :edition])] + (if-let [id (dm/get-in state [:workspace-local :edition])] (let [mode (dm/get-in state [:workspace-local :edit-path id :edit-mode])] (case mode :move (rx/of (common/finish-path)) :draw (rx/of (start-draw-mode)) - (rx/empty))))))) + (rx/empty))) + (when (and (= mode :move) (st/drawing? state)) + (rx/of (common/finish-path))))))) (defn reset-last-handler [] diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs index 6a4b620afa..c1d2269383 100644 --- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -157,6 +157,7 @@ :label (fn [] (tr "shortcuts.deselect-all")) :command (ds/c-mod "shift+a") :subsections [:path-editor] + :overwrite true :fn #(st/emit! (drp/deselect-all))} :flip-horizontal {:tooltip (ds/shift "H") diff --git a/frontend/test/frontend_tests/logic/path_lifecycle_test.cljs b/frontend/test/frontend_tests/logic/path_lifecycle_test.cljs index 1e88902f90..2dca2a405f 100644 --- a/frontend/test/frontend_tests/logic/path_lifecycle_test.cljs +++ b/frontend/test/frontend_tests/logic/path_lifecycle_test.cljs @@ -25,6 +25,7 @@ [app.main.data.workspace.path.streams :as path.streams] [app.main.data.workspace.path.tools :as path.tools] [app.main.data.workspace.path.undo :as path.undo] + [app.main.data.workspace.shortcuts :as wsc] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true] [frontend-tests.helpers.state :as ths] @@ -138,22 +139,23 @@ (defn- run-handle-drawing-end "Runs the draw-ending flow and passes its events to `callback`." - [restart? callback] - (let [state (pth/drawing-path-state) - stream (rx/subject) - emissions (atom [])] - (->> (ptk/watch (path.drawing/handle-drawing) state stream) - (rx/subs! #(swap! emissions conj %))) - (rx/push! stream (ptk/data-event ::path.drawing/end-edition - {:restart? restart?})) - ;; Wait for the asynchronous drawing-end event. - (js/setTimeout - (fn [] - (let [end-event (last @emissions) - end-emissions (atom [])] - (->> (ptk/watch end-event state stream) - (rx/subs! #(swap! end-emissions conj %))) - (callback @end-emissions)))))) + ([restart? callback] + (run-handle-drawing-end (pth/drawing-path-state) restart? callback)) + ([state restart? callback] + (let [stream (rx/subject) + emissions (atom [])] + (->> (ptk/watch (path.drawing/handle-drawing) state stream) + (rx/subs! #(swap! emissions conj %))) + (rx/push! stream (ptk/data-event ::path.drawing/end-edition + {:restart? restart?})) + ;; Wait for the asynchronous drawing-end event. + (js/setTimeout + (fn [] + (let [end-event (last @emissions) + end-emissions (atom [])] + (->> (ptk/watch end-event state stream) + (rx/subs! #(swap! end-emissions conj %))) + (callback @end-emissions))))))) (t/deftest escape-ending-new-path-draw-does-not-reenter-edition (t/async @@ -182,6 +184,43 @@ (mapv ptk/type emissions))) (done))))) +(t/deftest finishing-new-path-draw-in-move-mode-reenters-edition-in-move-mode + (t/async + done + (let [state (pth/drawing-path-state) + id (get-in state [:workspace-drawing :object :id]) + state (assoc-in state [:workspace-local :edit-path id :edit-mode] :move)] + (run-handle-drawing-end + state + true + (fn [emissions] + (let [edition-emissions (atom [])] + (->> (ptk/watch (last emissions) state nil) + (rx/subs! #(swap! edition-emissions conj %))) + (let [change-mode (last @edition-emissions) + edited {:workspace-local {:edition id + :edit-path {id {:edit-mode :draw}}}}] + (t/is (= ::path.drawing/change-edit-mode (ptk/type change-mode))) + (t/is (= :move (get-in (ptk/update change-mode edited) + [:workspace-local :edit-path id :edit-mode]))))) + (done)))))) + +(t/deftest move-mode-while-creating-path-finishes-it + (let [id (random-uuid) + state {:workspace-local + {:edit-path {id {:edit-mode :draw + :last-point (gpt/point 10 10)}}} + :workspace-drawing {:tool :path + :object {:id id :type :path}}} + event (path.drawing/change-edit-mode :move) + state' (ptk/update event state) + emissions (atom [])] + (t/is (= :move (get-in state' [:workspace-local :edit-path id :edit-mode]))) + (->> (ptk/watch event state' nil) + (rx/subs! #(swap! emissions conj %))) + (t/is (= [::path.common/finish-path] + (mapv ptk/type @emissions))))) + (t/deftest ending-a-draw-collapses-the-nodes-drawn-on-top-of-each-other (t/async done @@ -237,6 +276,12 @@ (t/is (= [::path.common/finish-path] (mapv ptk/type @emissions))))) +(t/deftest deselect-all-shortcut-replaces-the-grid-layout-toggle + (let [deselect-all (:deselect-all path.shortcuts/shortcuts) + grid-toggle (:toggle-layout-grid wsc/shortcuts)] + (t/is (= (:command grid-toggle) (:command deselect-all))) + (t/is (true? (:overwrite deselect-all))))) + (t/deftest escape-without-pending-segment-interrupts-edition (let [id (random-uuid) state {:workspace-local From 65b248eec38a4159fd493ef593d77b6ec52eac25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 7 Oct 2026 13:08:02 +0200 Subject: [PATCH 054/145] :bug: Preserve team ID in Nitrate membership URL (#12157) --- backend/src/app/nitrate.clj | 2 +- backend/test/backend_tests/nitrate_test.clj | 34 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 backend/test/backend_tests/nitrate_test.clj diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 15214ac449..d91772f0cd 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -275,7 +275,7 @@ (request-to-nitrate cfg :get (generate-nitrate-uri "api/teams/" - team-id + (str team-id "/") "users/" profile-id) schema:profile-organization params)) diff --git a/backend/test/backend_tests/nitrate_test.clj b/backend/test/backend_tests/nitrate_test.clj new file mode 100644 index 0000000000..c754539a95 --- /dev/null +++ b/backend/test/backend_tests/nitrate_test.clj @@ -0,0 +1,34 @@ +;; 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 SUBSIDIARY SL + +(ns backend-tests.nitrate-test + (:require + [app.config :as cf] + [app.nitrate] + [clojure.test :as t])) + +(def ^:private generate-nitrate-uri + (ns-resolve 'app.nitrate 'generate-nitrate-uri)) + +(t/deftest generate-nitrate-uri-test + (binding [cf/config (assoc cf/config + :admin-console-uri + "http://ac.example/admin-console")] + (doseq [[label segments expected] + [["single segment" + ["api/connectivity"] + "http://ac.example/admin-console/api/connectivity"] + ["resource identifier as the final segment" + ["api/teams/" "team-id"] + "http://ac.example/admin-console/api/teams/team-id"] + ["nested team membership path" + ["api/teams/" "team-id/" "users/" "profile-id"] + "http://ac.example/admin-console/api/teams/team-id/users/profile-id"] + ["nested organization membership path" + ["api/organizations/" "organization-id/" "members/" "profile-id"] + "http://ac.example/admin-console/api/organizations/organization-id/members/profile-id"]]] + (t/testing label + (t/is (= expected (apply generate-nitrate-uri segments))))))) From 6defe26a78fe5fbfc0d517b6c1b0426de08bb4e1 Mon Sep 17 00:00:00 2001 From: Danny Shirely <69107616+danialshirali16@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:48:46 +0330 Subject: [PATCH 055/145] :bug: Keep per-side stroke widths out of inspect geometry (#12055) Remove per-side stroke widths from both Inspect geometry panels so Stroke is the only section that lists them. Keep dimensions, position, corner radii, and transforms in Size and position. Add rendered-component regressions for boards, rectangles, multiple selection, uniform strokes, and CSS declarations. Closes #12026 AI-assisted-by: GPT-6 Signed-off-by: Danial Shirali Signed-off-by: Eva Marco Co-authored-by: Eva Marco --- .../main/ui/inspect/attributes/geometry.cljs | 4 - .../ui/inspect/styles/panels/geometry.cljs | 5 - frontend/test/frontend_tests/runner.cljs | 2 + .../ui/inspect_geometry_test.cljs | 114 ++++++++++++++++++ 4 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 frontend/test/frontend_tests/ui/inspect_geometry_test.cljs diff --git a/frontend/src/app/main/ui/inspect/attributes/geometry.cljs b/frontend/src/app/main/ui/inspect/attributes/geometry.cljs index 4f99f5dba8..477a6f01b0 100644 --- a/frontend/src/app/main/ui/inspect/attributes/geometry.cljs +++ b/frontend/src/app/main/ui/inspect/attributes/geometry.cljs @@ -22,10 +22,6 @@ :left :top :border-radius - :border-block-start-width ; top - :border-inline-end-width ; right - :border-block-end-width ; bottom - :border-inline-start-width ; left :transform]) (mf/defc geometry-block diff --git a/frontend/src/app/main/ui/inspect/styles/panels/geometry.cljs b/frontend/src/app/main/ui/inspect/styles/panels/geometry.cljs index 53d95fb00e..b5dca89400 100644 --- a/frontend/src/app/main/ui/inspect/styles/panels/geometry.cljs +++ b/frontend/src/app/main/ui/inspect/styles/panels/geometry.cljs @@ -22,11 +22,6 @@ :border-start-end-radius :border-end-start-radius :border-end-end-radius - ;; side strokes - :border-block-start-width - :border-inline-end-width - :border-block-end-width - :border-inline-start-width :transform]) (def ^:private shape-prop->border-radius-prop diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 598e9faac4..adb408ceb4 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -107,6 +107,7 @@ [frontend-tests.ui.ds-controls-numeric-input-test] [frontend-tests.ui.gradient-handlers-test] [frontend-tests.ui.history-test] + [frontend-tests.ui.inspect-geometry-test] [frontend-tests.ui.inspect-stroke-tokens-test] [frontend-tests.ui.layout-container-multiple-test] [frontend-tests.ui.measurements-test] @@ -243,6 +244,7 @@ 'frontend-tests.ui.ds-controls-numeric-input-test 'frontend-tests.ui.gradient-handlers-test 'frontend-tests.ui.history-test + 'frontend-tests.ui.inspect-geometry-test 'frontend-tests.ui.inspect-stroke-tokens-test 'frontend-tests.ui.layout-container-multiple-test 'frontend-tests.ui.measurements-test diff --git a/frontend/test/frontend_tests/ui/inspect_geometry_test.cljs b/frontend/test/frontend_tests/ui/inspect_geometry_test.cljs new file mode 100644 index 0000000000..3b9c5879d2 --- /dev/null +++ b/frontend/test/frontend_tests/ui/inspect_geometry_test.cljs @@ -0,0 +1,114 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.ui.inspect-geometry-test + (:require + ["react-dom/server" :as rds] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.main.ui.hooks :as hooks] + [app.main.ui.inspect.attributes.geometry :as geometry] + [app.main.ui.inspect.attributes.stroke :as stroke] + [app.main.ui.inspect.styles.panels.geometry :as styles-geometry] + [app.main.ui.inspect.styles.panels.stroke :as styles-stroke] + [app.util.code-gen.style-css :as css] + [cljs.test :as t :include-macros true] + [cuerdas.core :as str] + [frontend-tests.helpers.mock :as mock] + [rumext.v2 :as mf])) + +(def ^:private per-side-stroke + {:stroke-style :solid + :stroke-alignment :inner + :stroke-color "#000000" + :stroke-opacity 1 + :stroke-width 2 + :stroke-width-top 2 + :stroke-width-right 4 + :stroke-width-bottom 8 + :stroke-width-left 16}) + +(defn- shape + [type stroke] + {:id (uuid/next) + :type type + :parent-id uuid/zero + :frame-id uuid/zero + :selrect (grc/make-rect 0 0 120 80) + :points [(gpt/point 0 0) (gpt/point 120 0) + (gpt/point 120 80) (gpt/point 0 80)] + :transform (gmt/matrix) + :r1 5 :r2 5 :r3 5 :r4 5 + :strokes [stroke]}) + +(defn- render-panel + [panel shapes] + (rds/renderToStaticMarkup + (mf/element panel + #js {:shapes shapes + :objects (into {uuid/zero {:id uuid/zero + :type :frame + :selrect (grc/make-rect 0 0 120 80)}} + (map (juxt :id identity)) shapes) + :resolvedTokens {} + :colorSpace "hex" + :onGeometryShorthand identity + :onStrokeShorthand identity}))) + +(t/deftest geometry-panels-exclude-stroke-widths + (doseq [type [:frame :rect] + panel [geometry/geometry-panel* styles-geometry/geometry-panel*]] + (t/testing (str "geometry for " (name type)) + (let [markup (render-panel panel [(shape type per-side-stroke)])] + (t/is (not (re-find #"Border (block|inline).*? width" markup))) + (t/is (str/includes? markup "120px")) + (t/is (str/includes? markup "80px")) + (t/is (str/includes? markup "5px")))))) + +(t/deftest geometry-panels-exclude-stroke-widths-in-multiple-selection + (let [shapes [(shape :frame per-side-stroke) (shape :rect per-side-stroke)]] + (doseq [panel [geometry/geometry-panel* styles-geometry/geometry-panel*]] + (t/is (not (re-find #"Border (block|inline).*? width" + (render-panel panel shapes))))))) + +(t/deftest stroke-panels-keep-per-side-widths + (with-redefs [cf/flags (conj cf/flags :inspect-styles) + hooks/use-portal-container (mock/stub (fn [_] nil))] + (doseq [type [:frame :rect] + panel [stroke/stroke-panel* styles-stroke/stroke-panel*]] + (let [markup (render-panel panel [(shape type per-side-stroke)])] + (doseq [label ["Border block start width" "Border inline end width" + "Border block end width" "Border inline start width"]] + (t/is (str/includes? markup label))) + (doseq [value ["2px" "4px" "8px" "16px"]] + (t/is (str/includes? markup value))))))) + +(t/deftest code-view-keeps-each-per-side-width-once + (let [shape (shape :frame per-side-stroke) + code (css/get-shape-properties-css {(:id shape) shape} shape css/shape-css-properties)] + (doseq [declaration ["border-block-start-width: 2px;" + "border-inline-end-width: 4px;" + "border-block-end-width: 8px;" + "border-inline-start-width: 16px;"]] + (t/is (= 1 (count (re-seq (re-pattern declaration) code))))))) + +(t/deftest uniform-strokes-keep-width-in-the-stroke-panel + (let [uniform-stroke (-> per-side-stroke + (dissoc :stroke-width-top :stroke-width-right + :stroke-width-bottom :stroke-width-left) + (assoc :stroke-width 3)) + shapes [(shape :rect uniform-stroke)]] + (doseq [panel [geometry/geometry-panel* styles-geometry/geometry-panel*]] + (t/is (not (str/includes? (render-panel panel shapes) "Border width")))) + (with-redefs [cf/flags (conj cf/flags :inspect-styles) + hooks/use-portal-container (mock/stub (fn [_] nil))] + (doseq [panel [stroke/stroke-panel* styles-stroke/stroke-panel*]] + (let [markup (render-panel panel shapes)] + (t/is (str/includes? markup "Border width")) + (t/is (str/includes? markup "3px"))))))) From d0cf3998074eec0dd7ebb787ebe01b849d8755c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 7 Oct 2026 13:14:58 +0200 Subject: [PATCH 056/145] :bug: Show trial status on subscription page (#12158) --- frontend/src/app/main/ui/settings/subscription.cljs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs index b779fc2c70..f502f5285f 100644 --- a/frontend/src/app/main/ui/settings/subscription.cljs +++ b/frontend/src/app/main/ui/settings/subscription.cljs @@ -613,7 +613,9 @@ [:div {:class (stl/css :your-subscription)} [:h3 {:class (stl/css :plan-section-title)} (tr "subscription.settings.section-plan")] (if nitrate? - [:> plan-card* {:card-title (tr "subscription.settings.enterprise") + [:> plan-card* {:card-title (if (= (:status nitrate-license) "trialing") + (tr "subscription.settings.enterprise-trial") + (tr "subscription.settings.enterprise")) :cancel-at (when (:cancel-at nitrate-license) (tr "nitrate.subscription.active-until" (ct/format-inst (:cancel-at nitrate-license) "d MMMM, yyyy"))) :benefits-title (tr "subscription.settings.benefits.nitrate-unlimited-benefits") From 27ccb32ea1131dc193594078436106f7542aed55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 7 Oct 2026 11:06:26 +0000 Subject: [PATCH 057/145] :construction_worker: Fix the devenv image build workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI-assisted-by: Claude Signed-off-by: David Barragán Merino --- .github/workflows/build-docker-devenv.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docker-devenv.yml b/.github/workflows/build-docker-devenv.yml index 906adcd369..8b119c58f7 100644 --- a/.github/workflows/build-docker-devenv.yml +++ b/.github/workflows/build-docker-devenv.yml @@ -6,22 +6,34 @@ on: permissions: contents: read +concurrency: + group: build-docker-devenv + cancel-in-progress: false + jobs: build-and-push: name: Build and push DevEnv Docker image runs-on: penpot-standar-runner + timeout-minutes: 90 + env: + DOCKER_IMAGE: 'penpotapp/devenv' steps: - name: Set common environment variables run: | # Each job execution will use its own docker configuration. - echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV + echo "DOCKER_CONFIG=${RUNNER_TEMP}/.docker-${GITHUB_RUN_ID}-${GITHUB_JOB}" >> "$GITHUB_ENV" - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - name: Set up QEMU + uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 + with: + platforms: linux/amd64,linux/arm64 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 @@ -40,8 +52,6 @@ jobs: - name: Build and push DevEnv Docker image uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 - env: - DOCKER_IMAGE: 'penpotapp/devenv' with: context: ./docker/devenv/ file: ./docker/devenv/Dockerfile From c4c1c5a9559df51984a7c088067c5d1056d20db9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 7 Oct 2026 11:52:46 +0000 Subject: [PATCH 058/145] :bug: Fix media-processor upload-storage tests Update media-processor dependencies (multer 2.4, vitest 5 and others) and fix mockFile to emit Buffer chunks so multer memory storage Buffer.concat works. String chunks caused TypeError and 5s timeouts in two tests. AI-assisted-by: muse-spark-1.3-contributor --- media-processor/package.json | 22 +- media-processor/pnpm-lock.yaml | 639 ++++++++------------ media-processor/test/upload-storage.test.ts | 2 +- 3 files changed, 278 insertions(+), 385 deletions(-) diff --git a/media-processor/package.json b/media-processor/package.json index ee5114c7b9..f5373a591f 100644 --- a/media-processor/package.json +++ b/media-processor/package.json @@ -17,24 +17,24 @@ "packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45", "dependencies": { "express": "^5.2.1", - "multer": "^2.2.0", + "multer": "^2.4.0", "p-queue": "^9.3.3", - "pino": "^10.3.1", + "pino": "^10.4.0", "pino-loki": "^3.0.0", - "pino-pretty": "^13.1.3", - "sharp": "^0.35.4", - "zod": "^4.4.3" + "pino-pretty": "^13.2.0", + "sharp": "^0.35.5", + "zod": "^4.6.5" }, "devDependencies": { "@types/express": "^5.0.6", - "@types/multer": "^2.0.0", - "@types/node": "^26.3.0", + "@types/multer": "^2.3.0", + "@types/node": "^26.6.4", "@types/supertest": "^7.2.1", "esbuild": "^0.28.2", - "prettier": "^3.6.2", - "supertest": "^7.2.2", - "tsx": "^4.23.12", + "prettier": "^3.9.9", + "supertest": "^7.3.1", + "tsx": "^4.23.15", "typescript": "^7.0.2", - "vitest": "^4.1.11" + "vitest": "^5.0.3" } } diff --git a/media-processor/pnpm-lock.yaml b/media-processor/pnpm-lock.yaml index 926fab2cd0..56410eee5c 100644 --- a/media-processor/pnpm-lock.yaml +++ b/media-processor/pnpm-lock.yaml @@ -170,36 +170,36 @@ importers: specifier: ^5.2.1 version: 5.2.1 multer: - specifier: ^2.2.0 - version: 2.2.0 + specifier: ^2.4.0 + version: 2.4.0 p-queue: specifier: ^9.3.3 version: 9.3.3 pino: - specifier: ^10.3.1 - version: 10.3.1 + specifier: ^10.4.0 + version: 10.4.0 pino-loki: specifier: ^3.0.0 version: 3.0.0 pino-pretty: - specifier: ^13.1.3 - version: 13.1.3 + specifier: ^13.2.0 + version: 13.2.0 sharp: - specifier: ^0.35.4 - version: 0.35.4(@types/node@26.3.0) + specifier: ^0.35.5 + version: 0.35.5(@types/node@26.6.4) zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: ^4.6.5 + version: 4.6.5 devDependencies: '@types/express': specifier: ^5.0.6 version: 5.0.6 '@types/multer': - specifier: ^2.0.0 - version: 2.2.0 + specifier: ^2.3.0 + version: 2.3.0 '@types/node': - specifier: ^26.3.0 - version: 26.3.0 + specifier: ^26.6.4 + version: 26.6.4 '@types/supertest': specifier: ^7.2.1 version: 7.2.1 @@ -207,20 +207,20 @@ importers: specifier: ^0.28.2 version: 0.28.2 prettier: - specifier: ^3.6.2 - version: 3.9.6 + specifier: ^3.9.9 + version: 3.9.9 supertest: - specifier: ^7.2.2 - version: 7.2.2 + specifier: ^7.3.1 + version: 7.3.1 tsx: - specifier: ^4.23.12 - version: 4.23.12 + specifier: ^4.23.15 + version: 4.23.15 typescript: specifier: ^7.0.2 version: 7.0.2 vitest: - specifier: ^4.1.11 - version: 4.1.11(@types/node@26.3.0)(vite@7.3.6(@types/node@26.3.0)(tsx@4.23.12)) + specifier: ^5.0.3 + version: 5.0.3(@types/node@26.6.4)(vite@7.3.6(@types/node@26.6.4)(tsx@4.23.15)) packages: @@ -387,167 +387,177 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.4': - resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.4': - resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.4': - resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.3': - resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.3': - resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.3': - resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.3': - resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.3': - resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.3': - resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.3': - resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.3': - resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.3': - resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.3': - resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.4': - resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.4': - resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.4': - resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.4': - resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.4': - resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.4': - resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.4': - resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.4': - resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.4': - resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.4': - resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.4': - resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.4': - resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.4': - resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -696,9 +706,6 @@ packages: cpu: [x64] os: [win32] - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -729,11 +736,11 @@ packages: '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} - '@types/multer@2.2.0': - resolution: {integrity: sha512-3U1troeqGV8Ntp7Q3klwf4zr23VEoqYVocYXaswm9+8z3O9UHDYAqLxjJ/h550iRADTjKdOdhhasXw6gD6kYtg==} + '@types/multer@2.3.0': + resolution: {integrity: sha512-i7STIm9V4K2MPH4ZYMtrZAwNxs3kglk2LgleaTuB9pUXgADBcQYQuYMd7+6xgTIxfoggIkFA/DkkvycdZpIARA==} - '@types/node@26.3.0': - resolution: {integrity: sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==} + '@types/node@26.6.4': + resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} '@types/qs@6.15.1': resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} @@ -873,11 +880,8 @@ packages: cpu: [x64] os: [win32] - '@vitest/expect@4.1.11': - resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} - - '@vitest/mocker@4.1.11': - resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + '@vitest/mocker@5.0.3': + resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -887,20 +891,8 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.11': - resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - - '@vitest/runner@4.1.11': - resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} - - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - - '@vitest/utils@4.1.11': - resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + '@vitest/spy@5.0.3': + resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==} accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -927,9 +919,6 @@ packages: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -960,10 +949,6 @@ packages: component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} - concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} - content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -976,9 +961,6 @@ packages: resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} engines: {node: '>=18'} - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -1039,8 +1021,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} @@ -1168,8 +1150,8 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} @@ -1218,8 +1200,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multer@2.2.0: - resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} + multer@2.4.0: + resolution: {integrity: sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==} engines: {node: '>= 10.16.0'} nanoid@3.3.16: @@ -1265,9 +1247,6 @@ packages: path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1275,6 +1254,10 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} @@ -1283,23 +1266,23 @@ packages: engines: {node: '>=20'} hasBin: true - pino-pretty@13.1.3: - resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + pino-pretty@13.2.0: + resolution: {integrity: sha512-jjghD1ML+Di/WxjJDz6j6Y4FT7hIXOlb8h27AyJ+ohtAXJXH4PS/xkv7si+RCCIW59HMgZsMKTDgzdr0RhuRrg==} hasBin: true pino-std-serializers@7.1.0: resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} - pino@10.3.1: - resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + pino@10.4.0: + resolution: {integrity: sha512-bk1ZMTwG/Vymx+zPoa28MhNzucFvXJWp3csrSuxuHPSMeUettaCoQ5Jvg07mtC4JGNpcfTwz41oVCnNgBK9+vA==} hasBin: true postcss@8.5.20: resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} engines: {node: ^10 || ^12 || >=14} - prettier@3.9.6: - resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + prettier@3.9.9: + resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} engines: {node: '>=14'} hasBin: true @@ -1328,14 +1311,6 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - real-require@0.2.0: - resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} - engines: {node: '>= 12.13.0'} - real-require@1.0.0: resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} @@ -1348,9 +1323,6 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -1377,8 +1349,8 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.35.4: - resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} engines: {node: '>=20.9.0'} peerDependencies: '@types/node': '*' @@ -1402,12 +1374,12 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + sonic-boom@5.0.1: + resolution: {integrity: sha512-I1E9tHuKe6SdS/Uab4bl2o1ZUBm2SCTDFF9eQ66ehyVcFIBixfecy9DlwXCtnRF8CUzPw7Q4Dn6JQE/8iVl/FQ==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1416,9 +1388,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -1430,9 +1399,6 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-json-comments@5.0.3: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} @@ -1441,29 +1407,26 @@ packages: resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} engines: {node: '>=14.18.0'} - supertest@7.2.2: - resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + supertest@7.3.1: + resolution: {integrity: sha512-vNaNCFoCR/edDbErQZp5KLNT8R4dSPHX7rlDLoE8hWLAm/878j6ShMbe8HK/E6RwwuhlSXVelXAIg9AxR5WWPw==} engines: {node: '>=14.18.0'} thread-stream@4.2.0: resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} engines: {node: '>=20'} - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinybench@6.2.0: + resolution: {integrity: sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==} + engines: {node: '>=20.0.0'} - tinyexec@1.2.4: - resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} engines: {node: '>=18'} tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} - engines: {node: '>=14.0.0'} - toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -1471,8 +1434,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.23.12: - resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + tsx@4.23.15: + resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==} engines: {node: '>=18.0.0'} hasBin: true @@ -1484,24 +1447,18 @@ packages: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} - typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} hasBin: true - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -1546,23 +1503,23 @@ packages: yaml: optional: true - vitest@4.1.11: - resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@5.0.3: + resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.11 - '@vitest/browser-preview': 4.1.11 - '@vitest/browser-webdriverio': 4.1.11 - '@vitest/coverage-istanbul': 4.1.11 - '@vitest/coverage-v8': 4.1.11 - '@vitest/ui': 4.1.11 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-preview': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.3 + '@vitest/coverage-v8': 5.0.3 + '@vitest/ui': 5.0.3 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -1587,16 +1544,16 @@ packages: jsdom: optional: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} hasBin: true wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} snapshots: @@ -1685,112 +1642,121 @@ snapshots: '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.35.4': + '@img/sharp-darwin-arm64@0.35.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-arm64': 1.3.4 optional: true - '@img/sharp-darwin-x64@0.35.4': + '@img/sharp-darwin-x64@0.35.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.4 optional: true - '@img/sharp-freebsd-wasm32@0.35.4': + '@img/sharp-freebsd-wasm32@0.35.5': dependencies: - '@img/sharp-wasm32': 0.35.4 + '@img/sharp-wasm32': 0.35.5 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.3': + '@img/sharp-libvips-darwin-arm64@1.3.4': optional: true - '@img/sharp-libvips-darwin-x64@1.3.3': + '@img/sharp-libvips-darwin-x64@1.3.4': optional: true - '@img/sharp-libvips-linux-arm64@1.3.3': + '@img/sharp-libvips-linux-arm64@1.3.4': optional: true - '@img/sharp-libvips-linux-arm@1.3.3': + '@img/sharp-libvips-linux-arm@1.3.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.3': + '@img/sharp-libvips-linux-ppc64@1.3.4': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.3': + '@img/sharp-libvips-linux-riscv64@1.3.4': optional: true - '@img/sharp-libvips-linux-s390x@1.3.3': + '@img/sharp-libvips-linux-s390x@1.3.4': optional: true - '@img/sharp-libvips-linux-x64@1.3.3': + '@img/sharp-libvips-linux-x64@1.3.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.3': + '@img/sharp-libvips-linuxmusl-x64@1.3.4': optional: true - '@img/sharp-linux-arm64@0.35.4': + '@img/sharp-linux-arm64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.4 optional: true - '@img/sharp-linux-arm@0.35.4': + '@img/sharp-linux-arm@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.4 optional: true - '@img/sharp-linux-ppc64@0.35.4': + '@img/sharp-linux-ppc64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.4 optional: true - '@img/sharp-linux-riscv64@0.35.4': + '@img/sharp-linux-riscv64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.4 optional: true - '@img/sharp-linux-s390x@0.35.4': + '@img/sharp-linux-s390x@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.4 optional: true - '@img/sharp-linux-x64@0.35.4': + '@img/sharp-linux-x64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.4 optional: true - '@img/sharp-linuxmusl-arm64@0.35.4': + '@img/sharp-linuxmusl-arm64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 optional: true - '@img/sharp-linuxmusl-x64@0.35.4': + '@img/sharp-linuxmusl-x64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 optional: true - '@img/sharp-wasm32@0.35.4': + '@img/sharp-wasm32@0.35.5': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.4': + '@img/sharp-webcontainers-wasm32@0.35.5': dependencies: - '@img/sharp-wasm32': 0.35.4 + '@img/sharp-wasm32': 0.35.5 optional: true - '@img/sharp-win32-arm64@0.35.4': + '@img/sharp-win32-arm64@0.35.5': optional: true - '@img/sharp-win32-ia32@0.35.4': + '@img/sharp-win32-ia32@0.35.5': optional: true - '@img/sharp-win32-x64@0.35.4': + '@img/sharp-win32-x64@0.35.5': optional: true + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@noble/hashes@1.8.0': {} '@paralleldrive/cuid2@2.3.1': @@ -1874,12 +1840,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@standard-schema/spec@1.1.0': {} - '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 26.3.0 + '@types/node': 26.6.4 '@types/chai@5.2.3': dependencies: @@ -1888,7 +1852,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 26.3.0 + '@types/node': 26.6.4 '@types/cookiejar@2.1.5': {} @@ -1898,7 +1862,7 @@ snapshots: '@types/express-serve-static-core@5.1.2': dependencies: - '@types/node': 26.3.0 + '@types/node': 26.6.4 '@types/qs': 6.15.1 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -1913,13 +1877,13 @@ snapshots: '@types/methods@1.1.4': {} - '@types/multer@2.2.0': + '@types/multer@2.3.0': dependencies: '@types/express': 5.0.6 - '@types/node@26.3.0': + '@types/node@26.6.4': dependencies: - undici-types: 8.3.0 + undici-types: 8.9.0 '@types/qs@6.15.1': {} @@ -1927,18 +1891,18 @@ snapshots: '@types/send@1.2.1': dependencies: - '@types/node': 26.3.0 + '@types/node': 26.6.4 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 26.3.0 + '@types/node': 26.6.4 '@types/superagent@8.1.11': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 26.3.0 + '@types/node': 26.6.4 form-data: 4.0.6 '@types/supertest@7.2.1': @@ -2006,46 +1970,16 @@ snapshots: '@typescript/typescript-win32-x64@7.0.2': optional: true - '@vitest/expect@4.1.11': + '@vitest/mocker@5.0.3(vite@7.3.6(@types/node@26.6.4)(tsx@4.23.15))': dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - chai: 6.2.2 - tinyrainbow: 3.1.0 - - '@vitest/mocker@4.1.11(vite@7.3.6(@types/node@26.3.0)(tsx@4.23.12))': - dependencies: - '@vitest/spy': 4.1.11 + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.4.3 optionalDependencies: - vite: 7.3.6(@types/node@26.3.0)(tsx@4.23.12) + vite: 7.3.6(@types/node@26.6.4)(tsx@4.23.15) - '@vitest/pretty-format@4.1.11': - dependencies: - tinyrainbow: 3.1.0 - - '@vitest/runner@4.1.11': - dependencies: - '@vitest/utils': 4.1.11 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - '@vitest/utils': 4.1.11 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.11': {} - - '@vitest/utils@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 + '@vitest/spy@5.0.3': {} accepts@2.0.0: dependencies: @@ -2076,8 +2010,6 @@ snapshots: transitivePeerDependencies: - supports-color - buffer-from@1.1.2: {} - busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -2104,21 +2036,12 @@ snapshots: component-emitter@1.3.1: {} - concat-stream@2.0.0: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - typedarray: 0.0.6 - content-disposition@1.1.0: {} content-type@1.0.5: {} content-type@2.0.0: {} - convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -2160,7 +2083,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.3.1: {} + es-module-lexer@2.3.2: {} es-object-atoms@1.1.2: dependencies: @@ -2341,9 +2264,9 @@ snapshots: joycon@3.1.1: {} - magic-string@0.30.21: + magic-string@1.4.3: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 math-intrinsics@1.1.0: {} @@ -2373,11 +2296,10 @@ snapshots: ms@2.1.3: {} - multer@2.2.0: + multer@2.4.0: dependencies: append-field: 1.0.0 busboy: 1.6.0 - concat-stream: 2.0.0 type-is: 1.6.18 nanoid@3.3.16: {} @@ -2409,12 +2331,12 @@ snapshots: path-to-regexp@8.4.2: {} - pathe@2.0.3: {} - picocolors@1.1.1: {} picomatch@4.0.5: {} + picomatch@4.0.7: {} + pino-abstract-transport@3.0.0: dependencies: split2: 4.2.0 @@ -2424,7 +2346,7 @@ snapshots: pino-abstract-transport: 3.0.0 pump: 3.0.4 - pino-pretty@13.1.3: + pino-pretty@13.2.0: dependencies: colorette: 2.0.20 dateformat: 4.6.3 @@ -2437,12 +2359,12 @@ snapshots: pino-abstract-transport: 3.0.0 pump: 3.0.4 secure-json-parse: 4.1.0 - sonic-boom: 4.2.1 + sonic-boom: 5.0.1 strip-json-comments: 5.0.3 pino-std-serializers@7.1.0: {} - pino@10.3.1: + pino@10.4.0: dependencies: '@pinojs/redact': 0.4.0 atomic-sleep: 1.0.0 @@ -2451,7 +2373,7 @@ snapshots: pino-std-serializers: 7.1.0 process-warning: 5.0.0 quick-format-unescaped: 4.0.4 - real-require: 0.2.0 + real-require: 1.0.0 safe-stable-stringify: 2.5.0 sonic-boom: 4.2.1 thread-stream: 4.2.0 @@ -2462,7 +2384,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.9.6: {} + prettier@3.9.9: {} process-warning@5.0.0: {} @@ -2492,14 +2414,6 @@ snapshots: iconv-lite: 0.7.3 unpipe: 1.0.0 - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - real-require@0.2.0: {} - real-require@1.0.0: {} rollup@4.62.2: @@ -2543,8 +2457,6 @@ snapshots: transitivePeerDependencies: - supports-color - safe-buffer@5.2.1: {} - safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -2580,38 +2492,38 @@ snapshots: setprototypeof@1.2.0: {} - sharp@0.35.4(@types/node@26.3.0): + sharp@0.35.5(@types/node@26.6.4): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.4 - '@img/sharp-darwin-x64': 0.35.4 - '@img/sharp-freebsd-wasm32': 0.35.4 - '@img/sharp-libvips-darwin-arm64': 1.3.3 - '@img/sharp-libvips-darwin-x64': 1.3.3 - '@img/sharp-libvips-linux-arm': 1.3.3 - '@img/sharp-libvips-linux-arm64': 1.3.3 - '@img/sharp-libvips-linux-ppc64': 1.3.3 - '@img/sharp-libvips-linux-riscv64': 1.3.3 - '@img/sharp-libvips-linux-s390x': 1.3.3 - '@img/sharp-libvips-linux-x64': 1.3.3 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 - '@img/sharp-libvips-linuxmusl-x64': 1.3.3 - '@img/sharp-linux-arm': 0.35.4 - '@img/sharp-linux-arm64': 0.35.4 - '@img/sharp-linux-ppc64': 0.35.4 - '@img/sharp-linux-riscv64': 0.35.4 - '@img/sharp-linux-s390x': 0.35.4 - '@img/sharp-linux-x64': 0.35.4 - '@img/sharp-linuxmusl-arm64': 0.35.4 - '@img/sharp-linuxmusl-x64': 0.35.4 - '@img/sharp-webcontainers-wasm32': 0.35.4 - '@img/sharp-win32-arm64': 0.35.4 - '@img/sharp-win32-ia32': 0.35.4 - '@img/sharp-win32-x64': 0.35.4 - '@types/node': 26.3.0 + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 26.6.4 side-channel-list@1.0.1: dependencies: @@ -2641,28 +2553,24 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} - sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 + sonic-boom@5.0.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} split2@4.2.0: {} - stackback@0.0.2: {} - statuses@2.0.2: {} std-env@4.2.0: {} streamsearch@1.1.0: {} - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - strip-json-comments@5.0.3: {} superagent@10.3.0: @@ -2679,7 +2587,7 @@ snapshots: transitivePeerDependencies: - supports-color - supertest@7.2.2: + supertest@7.3.1: dependencies: cookie-signature: 1.2.2 methods: 1.1.2 @@ -2691,23 +2599,21 @@ snapshots: dependencies: real-require: 1.0.0 - tinybench@2.9.0: {} + tinybench@6.2.0: {} - tinyexec@1.2.4: {} + tinyexec@1.3.1: {} tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 - tinyrainbow@3.1.0: {} - toidentifier@1.0.1: {} tslib@2.8.1: optional: true - tsx@4.23.12: + tsx@4.23.15: dependencies: esbuild: 0.28.2 optionalDependencies: @@ -2724,8 +2630,6 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typedarray@0.0.6: {} - typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -2749,15 +2653,13 @@ snapshots: '@typescript/typescript-win32-arm64': 7.0.2 '@typescript/typescript-win32-x64': 7.0.2 - undici-types@8.3.0: {} + undici-types@8.9.0: {} unpipe@1.0.0: {} - util-deprecate@1.0.2: {} - vary@1.1.2: {} - vite@7.3.6(@types/node@26.3.0)(tsx@4.23.12): + vite@7.3.6(@types/node@26.6.4)(tsx@4.23.15): dependencies: esbuild: 0.28.2 fdir: 6.5.0(picomatch@4.0.5) @@ -2766,42 +2668,33 @@ snapshots: rollup: 4.62.2 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.3.0 + '@types/node': 26.6.4 fsevents: 2.3.3 - tsx: 4.23.12 + tsx: 4.23.15 - vitest@4.1.11(@types/node@26.3.0)(vite@7.3.6(@types/node@26.3.0)(tsx@4.23.12)): + vitest@5.0.3(@types/node@26.6.4)(vite@7.3.6(@types/node@26.6.4)(tsx@4.23.15)): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@7.3.6(@types/node@26.3.0)(tsx@4.23.12)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - es-module-lexer: 2.3.1 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(vite@7.3.6(@types/node@26.6.4)(tsx@4.23.15)) + chai: 6.2.2 + es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 + magic-string: 1.4.3 obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 + picomatch: 4.0.7 std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 + tinybench: 6.2.0 + tinyexec: 1.3.1 tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 7.3.6(@types/node@26.3.0)(tsx@4.23.12) - why-is-node-running: 2.3.0 + vite: 7.3.6(@types/node@26.6.4)(tsx@4.23.15) + why-is-node-running: 3.2.1 optionalDependencies: - '@types/node': 26.3.0 + '@types/node': 26.6.4 transitivePeerDependencies: - msw - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 + why-is-node-running@3.2.1: {} wrappy@1.0.2: {} - zod@4.4.3: {} + zod@4.6.5: {} diff --git a/media-processor/test/upload-storage.test.ts b/media-processor/test/upload-storage.test.ts index b2613e533a..79a8250535 100644 --- a/media-processor/test/upload-storage.test.ts +++ b/media-processor/test/upload-storage.test.ts @@ -13,7 +13,7 @@ function mockReq(contentLength?: string): Request { } function mockFile(content: string = "test content") { - const stream = Readable.from([content]); + const stream = Readable.from([Buffer.from(content)]); return { fieldname: "file", originalname: "test.txt", From ddce3c2c95bade1af35d6afbfd00ae4c90088829 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 7 Oct 2026 14:03:08 +0200 Subject: [PATCH 059/145] :bug: Open the import dialog again from template links (#12131) Template links (#?template=) keep their params in a bare "#?" fragment. Since routing moved to the query string, only "#/" legacy hashes are translated, and the root route was an empty query string, so the template param was dropped and the dashboard opened without the import dialog. Route a "#?" fragment as the query token, and treat a query without a screen param as the root, as before. Logged out users also lost the template: get-teams requires a session and its 401 ended the flow before the params were kept for after login. Skip it for the anonymous profile. This part already failed on main. Closes #12127 AI-assisted-by: claude-opus-5-5 --- frontend/src/app/main/ui/routes.cljs | 31 ++++++++-- .../test/frontend_tests/ui/routes_test.cljs | 60 +++++++++++++++++++ 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/main/ui/routes.cljs b/frontend/src/app/main/ui/routes.cljs index a4cb7fedab..8096051d40 100644 --- a/frontend/src/app/main/ui/routes.cljs +++ b/frontend/src/app/main/ui/routes.cljs @@ -140,6 +140,13 @@ (u/query-string->map (:query uri)))}))) +(defn- legacy-query-token + "Legacy root links (e.g. `#?template=`) keep their params in a + bare `#?…` fragment. Return them as a query token, or nil." + [hash] + (when (str/starts-with? hash "#?") + (subs hash 1))) + (defn- store-session-params [{:keys [template plugin]}] (binding [storage/*sync* true] @@ -226,7 +233,8 @@ (let [location (.-location js/document) location-path (dm/str (.-origin location) (.-pathname location)) valid-location? (= location-path (dm/str cf/public-uri)) - legacy-hash (.-hash location)] + legacy-hash (.-hash location) + legacy-token (legacy-query-token legacy-hash)] (cond (not valid-location?) @@ -242,6 +250,10 @@ (st/emit! (rt/nav name params {::rt/replace true})) (on-query-navigate router token send-event-info?)) + ;; TODO(next-version): delete with `legacy-routes`. + (some? legacy-token) + (on-query-navigate router legacy-token send-event-info?) + :else (on-query-navigate router token send-event-info?)))) @@ -251,7 +263,10 @@ (subs token 1) (or token "")) query-params (u/query-string->map token-query) - empty-token? (str/blank? token-query) + ;; Without a `screen` the token targets the app root; its params + ;; (e.g. `template`) are forwarded to the dashboard or kept for + ;; after login. + root? (nil? (rt/get-query-param query-params :screen)) match (rt/match router token)] (if (some? match) (handle-sso-error-and-navigate match send-event-info? (rt/get-current-href)) @@ -261,9 +276,13 @@ ;; on invitations workflows (and probably other cases). (->> (rp/cmd! :get-profile) (rx/mapcat (fn [profile] - (->> (rp/cmd! :get-teams {}) - (rx/map (fn [teams] - (assoc profile ::teams (into #{} (map :id) teams))))))) + ;; get-teams requires a session; anonymous users + ;; only need their params kept for after login. + (if (= (:id profile) uuid/zero) + (rx/of profile) + (->> (rp/cmd! :get-teams {}) + (rx/map (fn [teams] + (assoc profile ::teams (into #{} (map :id) teams)))))))) (rx/subs! (fn [{:keys [id ::teams] :as profile}] (cond (= id uuid/zero) @@ -271,7 +290,7 @@ (store-session-params query-params) (st/emit! (rt/nav :auth-login))) - empty-token? + root? (let [default-team-id (:default-team-id profile) last-team-id (dtm/get-last-team-id) team-id (if (contains? teams last-team-id) diff --git a/frontend/test/frontend_tests/ui/routes_test.cljs b/frontend/test/frontend_tests/ui/routes_test.cljs index e85246d954..68ef15c5d1 100644 --- a/frontend/test/frontend_tests/ui/routes_test.cljs +++ b/frontend/test/frontend_tests/ui/routes_test.cljs @@ -9,9 +9,11 @@ [app.common.time :as ct] [app.common.uuid :as uuid] [app.config :as cf] + [app.main.data.team :as dtm] [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.routes :as routes] + [app.util.storage :as storage] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true] [frontend-tests.helpers.mock :as mock])) @@ -95,3 +97,61 @@ (t/is (= 2 (count @events))) (done')) done)))) + +;; Template links (`#?template=`) predate query-string routing: the +;; params live in a bare `#?…` fragment and there is no `screen` (#12127). + +(def ^:private template-url + "https://penpot.github.io/penpot-files/tutorial-for-beginners%20v.2.0.penpot") + +(def ^:private template-param + "The query param as the router decodes it." + "https://penpot.github.io/penpot-files/tutorial-for-beginners v.2.0.penpot") + +(t/deftest legacy-query-hash-becomes-a-query-token + (t/is (= (str "?template=" template-url) + (#'routes/legacy-query-token (str "#?template=" template-url)))) + (t/is (nil? (#'routes/legacy-query-token "#/workspace?team-id=1"))) + (t/is (nil? (#'routes/legacy-query-token "")))) + +(t/deftest query-without-screen-goes-to-dashboard-with-its-params + (t/async done + (let [team-id (uuid/next) + events (atom [])] + (mock/with-mocks + {rp/cmd! (mock/stub + (fn [command _] + (case command + :get-profile (rx/of {:id (uuid/next) :default-team-id team-id}) + :get-teams (rx/of [{:id team-id}])))) + dtm/get-last-team-id (mock/stub (fn [] nil)) + st/emit! (mock/stub (fn [& emitted] (swap! events into emitted)))} + (fn [done'] + (#'routes/on-query-navigate #{:dashboard-recent} (str "?template=" template-url) false) + (let [{:keys [id params]} (some-> (first @events) deref)] + (t/is (= :dashboard-recent id)) + (t/is (= template-param (:template params))) + (t/is (= team-id (:team-id params)))) + (done')) + done)))) + +(t/deftest query-without-screen-keeps-template-for-after-login + (t/async done + (let [events (atom [])] + (mock/with-mocks + {rp/cmd! (mock/stub + (fn [command _] + (case command + :get-profile (rx/of {:id uuid/zero}) + ;; The backend requires a session for get-teams. + :get-teams (rx/throw (ex-info "authentication required" + {:type :authentication}))))) + st/emit! (mock/stub (fn [& emitted] (swap! events into emitted)))} + (fn [done'] + (#'routes/on-query-navigate #{:auth-login} (str "?template=" template-url) false) + (t/is (= template-param (:template @storage/session))) + (t/is (= :auth-login (:id (some-> (first @events) deref)))) + (binding [storage/*sync* true] + (swap! storage/session dissoc :template)) + (done')) + done)))) From 6473dc85abbb352e2cbda40e2d3c6e409e157d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 7 Oct 2026 12:06:06 +0000 Subject: [PATCH 060/145] :construction_worker: Notify new devenv images in penpotdev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: David Barragán Merino --- .github/workflows/build-docker-devenv.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-docker-devenv.yml b/.github/workflows/build-docker-devenv.yml index 8b119c58f7..a1c99e2bdd 100644 --- a/.github/workflows/build-docker-devenv.yml +++ b/.github/workflows/build-docker-devenv.yml @@ -71,4 +71,13 @@ jobs: TEXT: | 🚀 *[PENPOT] New devenv available* 📄 You may want to update your devenv. - @alvaro + + - name: Notify Mattermost (developers) + uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0 + with: + MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }} + MATTERMOST_CHANNEL: penpotdev + TEXT: | + 🚀 *[PENPOT] New devenv available* + 📄 You may want to update your devenv. + (\cc @alvaro) From 31000c3055bc5477e31b4bb130e956876c3352bc Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 7 Oct 2026 14:52:46 +0200 Subject: [PATCH 061/145] :zap: Enable rustc wasm SIMD for render-wasm builds (#12163) Set RUSTFLAGS target-feature=+simd128 on the emscripten target so our Rust code can autovec; Skia SIMD remains via the -simd prebuild and EMCC -msimd128. --- render-wasm/_build_env | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/render-wasm/_build_env b/render-wasm/_build_env index 2673c81ba4..0ed8f2bfe4 100644 --- a/render-wasm/_build_env +++ b/render-wasm/_build_env @@ -22,6 +22,13 @@ export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"}; export CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-"target/$RENDER_TARGET"}; export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.153.3/skia-binaries-b7f043e0b1e2a850e702-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp-pdf-simd.tar.gz"} +# Let rustc emit wasm SIMD for our Rust code (autovec / core::arch::wasm32). +# Skia SIMD still comes from the -simd prebuild + -msimd128 in EMCC_CFLAGS. +# Gate on the emscripten target so native `./test` is unaffected. +if [ "$CARGO_BUILD_TARGET" = "wasm32-unknown-emscripten" ]; then + export RUSTFLAGS="${RUSTFLAGS:-} -C target-feature=+simd128" +fi + # 256 MB of initial heap to perform less # initial calls to memory grow. export EM_INITIAL_HEAP=$((256 * 1024 * 1024)) From be17fabc6a5a013efc749f0bf44942854ab10c3c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 2 Oct 2026 12:20:51 +0000 Subject: [PATCH 062/145] :tada: Introduce global plugins and move MCP to application scope Global plugin lifecycle: - Add optional manifest scope "global" for plugins that remain active across dashboard and workspace navigation within one browser tab. Existing plugins retain workspace scope by default. - Preserve global sandboxes when a workspace ends or another plugin loads, including when that other plugin fails to initialize. - Avoid duplicate instances of an already-running global plugin. - Close global plugins on explicit closure or logout, with idempotent cleanup of listeners, timers and UI resources. Global scope does not imply automatic startup or persistence across a browser reload. Plugin API and navigation: - Keep a permanently live penpot facade whose getters follow the current workspace, while user, theme and UI facilities remain available on the dashboard. - Expose penpotMgmt to global plugins with workspace metadata, none/loading/ready status and workspacechange subscriptions. - Add awaitable openFile(fileId, {teamId}) navigation in the current tab, defaulting to the current team and resolving only once the requested file and its active page are ready. - Reject invalid identifiers, failed or superseded navigation and waits exceeding 30 seconds; release navigation watchers on completion. - Guard workspace-bound operations while no workspace is ready and return null from root, currentFile and currentPage in that state. - Require content:read for the initial management API. Callers must reacquire file, page and shape references after changing files. MCP lifecycle and connection controls: - Make MCP a global plugin and initialize its integrated controller per authenticated application session instead of per workspace. - Load the enabled integration with a valid token on the dashboard as well as in a file, retaining its existing hidden-iframe implementation. - Keep enabling and connecting separate: an enabled plugin starts idle, and each tab connects explicitly through dashboard or workspace controls. - Preserve the iframe, WebSocket, heartbeat and reconnect machinery during navigation; disconnect on request and close on disable/logout. MCP sessions and execution: - Derive the short MCP session ID solely from currentUser.sessionId, excluding the file ID. Retain the SHA-256-based, 50-bit Base32 encoding to produce a stable, copyable 10-character identifier. - Allow sessions without an open file and update workspace metadata over the existing connection without replacing its routing identity. - Refresh context after the connection handshake so initialization does not leave stale file metadata. - Expose penpotMgmt to execute_code, preserve execution storage across navigation and skip layout waits when no workspace is ready. - Update API types, tool guidance, documentation and the changelog. Validation: - Cover authenticated startup, workspace survival, logout cleanup, readiness, navigation failure, duplicate loads and session routing. - Verify that short IDs remain stable across files and reconnects and that disconnect cancels pending asynchronous ID initialization. - Pass 16 focused frontend tests, 73 runtime tests, 30 MCP plugin tests, builds, TypeScript checks and frontend/runtime lint. - Pass all 73 MCP server tests with --test-force-exit; the normal runner still stays alive after assertions finish. - Verify a live dashboard-to-file-to-dashboard round trip without a document or iframe reload, preserving facade identity and storage. AI-assisted-by: gpt-6 --- common/src/app/common/types/plugins.cljc | 1 + docs/mcp/index.md | 9 +- frontend/src/app/main.cljs | 2 + frontend/src/app/main/data/plugins.cljs | 6 +- frontend/src/app/main/data/workspace.cljs | 13 +-- frontend/src/app/main/data/workspace/mcp.cljs | 30 ++++++- .../src/app/main/ui/dashboard/sidebar.cljs | 23 +++++ frontend/src/app/plugins/api.cljs | 4 + frontend/src/app/plugins/events.cljs | 20 ++++- frontend/src/app/plugins/management.cljs | 78 +++++++++++++++++ frontend/src/app/plugins/register.cljs | 1 + .../data/workspace_mcp_test.cljs | 37 +++++++- .../plugins/management_test.cljs | 83 ++++++++++++++++++ frontend/test/frontend_tests/runner.cljs | 2 + mcp/README.md | 12 ++- mcp/packages/common/src/types.ts | 5 +- mcp/packages/plugin/public/manifest.json | 1 + .../plugin/src/ConnectionMetadata.test.ts | 71 ++++++++++++--- mcp/packages/plugin/src/SessionId.test.ts | 14 +-- mcp/packages/plugin/src/SessionId.ts | 4 +- mcp/packages/plugin/src/index.d.ts | 1 + mcp/packages/plugin/src/main.ts | 12 ++- mcp/packages/plugin/src/plugin.ts | 30 ++++++- .../task-handlers/ExecuteCodeTaskHandler.ts | 3 +- .../server/src/PluginBridge.sessions.test.ts | 26 ++++++ mcp/packages/server/src/PluginBridge.ts | 12 ++- .../server/src/dispatch/TaskDispatcher.ts | 2 +- .../server/src/tools/ExecuteCodeTool.ts | 6 +- plugins/CHANGELOG.md | 2 + plugins/libs/plugin-types/index.d.ts | 27 +++++- .../libs/plugins-runtime/src/lib/api/index.ts | 56 ++++++++++-- .../src/lib/create-sandbox.spec.ts | 1 + .../plugins-runtime/src/lib/create-sandbox.ts | 9 +- .../src/lib/load-plugin-real-path.spec.ts | 86 ++++++++++++++++++- .../plugins-runtime/src/lib/load-plugin.ts | 38 +++++--- .../src/lib/models/manifest.schema.ts | 1 + .../plugins-runtime/src/lib/plugin-manager.ts | 13 +-- 37 files changed, 647 insertions(+), 94 deletions(-) create mode 100644 frontend/src/app/plugins/management.cljs create mode 100644 frontend/test/frontend_tests/plugins/management_test.cljs diff --git a/common/src/app/common/types/plugins.cljc b/common/src/app/common/types/plugins.cljc index 035be0efb3..b9a1ca5330 100644 --- a/common/src/app/common/types/plugins.cljc +++ b/common/src/app/common/types/plugins.cljc @@ -58,6 +58,7 @@ [:map [:plugin-id :string] [:version {:optional true} :int] + [:scope {:optional true} [:enum "workspace" "global"]] [:name [:string {:max (:name registry-entry-max-lengths)}]] [:description {:optional true} [:string {:max (:description registry-entry-max-lengths)}]] [:host [:string {:max (:host registry-entry-max-lengths)}]] diff --git a/docs/mcp/index.md b/docs/mcp/index.md index 815a468fea..47a4c55803 100644 --- a/docs/mcp/index.md +++ b/docs/mcp/index.md @@ -74,16 +74,17 @@ There are three key pieces: Some important concepts for users: * **Integrations page**: MCP is configured under **Your account → Integrations → MCP Server**. Here you enable or disable MCP, get the server URL and manage the MCP key. * **MCP key**: a personal, non-recoverable token that authenticates your AI client with the MCP server. Only one key can exist per user at a time. This is used by the remote MCP setup. -* **MCP session**: a connection from a Penpot tab to the MCP server, identified by a short session ID. Several tabs can connect independently, including tabs showing the same file. +* **MCP session**: a connection from a Penpot tab to the MCP server, identified by a short session ID derived from the tab's user session. Several tabs can connect independently, including tabs showing the same file. * **Current page**: the page open in the selected session. Switching browser focus to another tab does not change which session the agent uses. ### Working with multiple files Connect the tabs you want to use: -* **Remote MCP**: open each file and choose **MCP → Connect** in the toolbar. Use **Disconnect** in the same menu to disconnect that tab. +* **Remote MCP**: choose **MCP: Connect** in the dashboard sidebar or **MCP → Connect** in a file's toolbar. Use **Disconnect** in the same place to disconnect that tab. * **Local MCP**: run the plugin in each file and connect it to your local server. Each plugin connection has its own session ID. * Connecting or disconnecting one tab does not disconnect the others. +* A connected tab stays connected while navigating between the dashboard and files. Its session ID stays the same; requests operate on the currently open file. Choose a session for your agent: @@ -176,7 +177,7 @@ When this happens, MCP fails fast instead of waiting for a long task timeout: To recover: * If the tab was only suspended, focus it, wait until MCP reconnects, and retry the prompt. -* If the tab was unloaded or reloaded, open the file and connect again. Copy the new session ID if your agent was using the previous one. +* If the tab was unloaded or reloaded, connect again from the dashboard or a file. Copy the new session ID if your agent was using the previous one. To reduce the chances of the browser putting Penpot to sleep during long MCP sessions: @@ -271,7 +272,7 @@ Remote MCP is the easiest way to start using AI agents with Penpot. It's hosted 1. Open **Your account → Integrations**. 2. In the **MCP Server** section, read the short description to confirm that feature is available for your account. -3. Use the **Status** toggle to enable MCP Server. Penpot remembers this state per user across sessions. You still need to connect each file explicitly. +3. Use the **Status** toggle to enable MCP Server. Penpot remembers this state per user across sessions. You still need to connect each tab explicitly. 4. If this is your first time, Penpot will ask you to **generate an MCP key**. The key is shown only once, store it safely. * Treat the MCP key like a password/token: do not share it in screenshots, logs, or code samples. 5. Once enabled, you will see: diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index c7d87ba8ba..06ffc849da 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -16,6 +16,7 @@ [app.main.data.event :as ev] [app.main.data.profile :as dp] [app.main.data.websocket :as ws] + [app.main.data.workspace.mcp :as mcp] [app.main.errors] [app.main.features :as feat] [app.main.rasterizer :as thr] @@ -81,6 +82,7 @@ (watch [_ _ stream] (rx/merge (rx/of (ev/initialize) + (mcp/initialize) (dp/refresh-profile)) ;; Watch for profile deletion events diff --git a/frontend/src/app/main/data/plugins.cljs b/frontend/src/app/main/data/plugins.cljs index 33d898cf69..e9b15e0849 100644 --- a/frontend/src/app/main/data/plugins.cljs +++ b/frontend/src/app/main/data/plugins.cljs @@ -58,7 +58,7 @@ (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] + [{:keys [plugin-id name version description host code permissions allow-background scope]} ^js extensions] (let [load-plugin (unchecked-get ug/global "ɵloadPlugin")] (if (fn? load-plugin) (-> (load-plugin @@ -69,6 +69,7 @@ :host host :code code :allowBackground (boolean allow-background) + :scope scope :permissions (apply array permissions)} nil extensions) @@ -82,7 +83,7 @@ :action "start-plugin!")))) (defn- load-plugin! - [{:keys [plugin-id name version description host code icon permissions]}] + [{:keys [plugin-id name version description host code icon permissions scope]}] (st/emit! (pflag/initialize plugin-id version) (save-current-plugin plugin-id)) @@ -96,6 +97,7 @@ :host host :code code :icon icon + :scope scope :permissions (apply array permissions)} (fn [] (st/emit! (remove-current-plugin plugin-id)))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index b04bf5394b..e4b79d7b66 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -55,7 +55,6 @@ [app.main.data.workspace.layers :as dwly] [app.main.data.workspace.layout :as layout] [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.mcp :as mcp] [app.main.data.workspace.notifications :as dwn] [app.main.data.workspace.pages :as dwpg] [app.main.data.workspace.path :as dwdp] @@ -77,7 +76,6 @@ [app.main.refs :as refs] [app.main.repo :as rp] [app.main.router :as rt] - [app.plugins.register :as preg] [app.render-wasm :as wasm] [app.render-wasm.api :as wasm.api] [app.util.dom :as dom] @@ -259,15 +257,8 @@ ptk/WatchEvent (watch [_ _ _] - (rx/merge - (rx/of (dp/check-open-plugin) - (fdf/fix-deleted-fonts-for-local-library file-id)) - (if (contains? cf/flags :mcp) - ;; We wait the plugin runtime to be ready before launch the - ;; mcp initialization - (->> (rx/from (preg/wait-for-runtime)) - (rx/map (fn [_] (mcp/init)))) - (rx/empty)))))) + (rx/of (dp/check-open-plugin) + (fdf/fix-deleted-fonts-for-local-library file-id))))) (defn- compute-shape-stats "Compute shape statistics in a single pass over pages-index. diff --git a/frontend/src/app/main/data/workspace/mcp.cljs b/frontend/src/app/main/data/workspace/mcp.cljs index 74884289c4..f3e3817239 100644 --- a/frontend/src/app/main/data/workspace/mcp.cljs +++ b/frontend/src/app/main/data/workspace/mcp.cljs @@ -12,13 +12,14 @@ [app.common.uri :as u] [app.config :as cf] [app.main.broadcast :as mbc] + [app.main.data.auth :as-alias auth] [app.main.data.plugins :as dp] [app.main.data.profile :as du] - [app.main.data.workspace :as-alias dw] [app.main.store :as st] [app.plugins.register :as preg] [app.util.timers :as ts] [beicon.v2.core :as rx] + [beicon.v2.operators :as rxo] [potok.v2.core :as ptk])) (def reconnect-fallback-interval 60000) @@ -36,6 +37,7 @@ :plugin-id preg/mcp-plugin-id :description "This plugin enables interaction with the Penpot MCP server" :allow-background true + :scope "global" :permissions #{"user:read" "library:read" "library:write" @@ -147,7 +149,7 @@ (assoc :host (str (u/join cf/public-uri "plugins/mcp/")))) stopper-s (rx/merge - (rx/filter (ptk/type? ::dw/finalize-workspace) stream) + (rx/filter (ptk/type? ::auth/logged-out) stream) (rx/filter (ptk/type? ::stop-mcp-plugin) stream) (rx/filter (ptk/type? ::init) stream)) @@ -234,7 +236,7 @@ ptk/WatchEvent (watch [_ state stream] (let [stopper-s (rx/merge - (rx/filter (ptk/type? ::dw/finalize-workspace) stream) + (rx/filter (ptk/type? ::auth/logged-out) stream) (rx/filter (ptk/type? ::init) stream)) mcp-state (get state :mcp)] @@ -274,3 +276,25 @@ (stop-mcp-plugin)))))) (rx/take-until stopper-s)))))) + +(defn initialize + "Initialize the MCP controller for each authenticated app session." + [] + (ptk/reify ::initialize + ptk/WatchEvent + (watch [_ _ stream] + (if (contains? cf/flags :mcp) + (->> stream + (rx/filter #(or (ptk/type? ::du/set-profile %) + (ptk/type? ::auth/logged-out %))) + (rx/map #(when (ptk/type? ::du/set-profile %) + (:id (deref %)))) + (rx/pipe (rxo/distinct-contiguous)) + (rx/switch-map + (fn [profile-id] + (if profile-id + (->> (rx/from (preg/wait-for-runtime)) + (rx/map (fn [_] (init)))) + (rx/empty)))) + (rx/take-until (rx/filter (ptk/type? ::initialize) stream))) + (rx/empty))))) diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 2f37187244..386d1dc791 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -17,6 +17,7 @@ [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] [app.main.data.team :as dtm] + [app.main.data.workspace.mcp :as mcp] [app.main.refs :as refs] [app.main.router :as rt] [app.main.store :as st] @@ -255,6 +256,13 @@ (get default-project :id) team-id (get team :id) + mcp-state (mf/deref refs/mcp) + mcp-requested? (:connection-requested mcp-state) + toggle-mcp (mf/use-fn + (mf/deps mcp-requested?) + #(st/emit! (if mcp-requested? + (mcp/user-disconnect-mcp) + (mcp/connect-mcp)))) projects? (= section :dashboard-recent) fonts? (= section :dashboard-fonts) @@ -382,6 +390,21 @@ [:span {:class (stl/css :element-title)} (tr "labels.shared-libraries")]]]]] + (when (and (contains? cf/flags :mcp) + (:enabled mcp-state) + (:token-valid mcp-state)) + [:div {:class (stl/css :sidebar-content-section)} + [:ul {:class (stl/css :sidebar-nav)} + [:li {:class (stl/css :sidebar-nav-item)} + [:> link* {:action toggle-mcp + :keyboard-action toggle-mcp + :class (stl/css :sidebar-link)} + [:span {:class (stl/css :element-title)} + "MCP: " + (if mcp-requested? + (tr "workspace.header.menu.mcp.plugin.status.disconnect") + (tr "workspace.header.menu.mcp.plugin.status.connect"))]]]]]) + [:div {:class (stl/css :sidebar-content-section) :data-testid "pinned-projects"} [:div {:class (stl/css :sidebar-section-title)} diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index 226b324c1f..2f0944f800 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -43,6 +43,7 @@ [app.plugins.history :as history] [app.plugins.library :as library] [app.plugins.local-storage :as local-storage] + [app.plugins.management :as management] [app.plugins.page :as page] [app.plugins.parser :as parser] [app.plugins.reflow :as wrfp] @@ -88,6 +89,9 @@ :$plugin {:enumerable false :get (fn [] plugin-id)} ;; Public properties + :management + {:get (fn [] (management/create-context))} + :version {:this true :get (constantly (:base cf/version))} diff --git a/frontend/src/app/plugins/events.cljs b/frontend/src/app/plugins/events.cljs index 884a1e0bac..51ac7c596f 100644 --- a/frontend/src/app/plugins/events.cljs +++ b/frontend/src/app/plugins/events.cljs @@ -10,6 +10,7 @@ [app.main.data.helpers :as dsh] [app.main.store :as st] [app.plugins.file :as file] + [app.plugins.management :as management] [app.plugins.page :as page] [app.plugins.parser :as parser] [app.plugins.shape :as shape] @@ -19,6 +20,21 @@ (defmulti handle-state-change (fn [type _ _ _ _] type)) +(defmethod handle-state-change "workspacechange" + [_ _ old-val new-val _] + (let [old-workspace (management/workspace-context old-val) + new-workspace (management/workspace-context new-val)] + (if (= old-workspace new-workspace) + ::not-changed + (clj->js new-workspace)))) + +(defmethod handle-state-change "logout" + [_ _ old-val new-val _] + (if (and (:profile-id old-val) + (not= (:profile-id old-val) (:profile-id new-val))) + ::void + ::not-changed)) + (defmethod handle-state-change "finish" [_ _ old-val new-val _] (let [old-file-id (:current-file-id old-val) @@ -123,7 +139,9 @@ (try (let [result (handle-state-change type plugin-id old-val new-val props)] (when (not= ::not-changed result) - (debounced-callback result))) + (if (contains? #{"workspacechange" "logout"} type) + (safe-callback result) + (debounced-callback result)))) (catch :default cause (.error js/console cause))))) diff --git a/frontend/src/app/plugins/management.cljs b/frontend/src/app/plugins/management.cljs new file mode 100644 index 0000000000..bff5d9292d --- /dev/null +++ b/frontend/src/app/plugins/management.cljs @@ -0,0 +1,78 @@ +;; 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 SUBSIDIARY SL + +(ns app.plugins.management + (:require + [app.common.uuid :as uuid] + [app.main.data.helpers :as dsh] + [app.main.router :as rt] + [app.main.store :as st] + [app.util.object :as obj])) + +(defn workspace-context + [state] + (let [params (rt/get-params state) + file-id (when (= :workspace (rt/lookup-name state)) + (uuid/parse* (:file-id params))) + ready? (and file-id + (nil? (:exception state)) + (= file-id (:current-file-id state) (:workspace-ready state)) + (some? (:current-page-id state)) + (some? (dsh/lookup-page state file-id (:current-page-id state))))] + {:status (cond ready? "ready" file-id "loading" :else "none") + :fileId (some-> file-id str) + :fileName (when file-id (:name (dsh/lookup-file state file-id))) + :teamId (some-> (:current-team-id state) str)})) + +(defn open-file + [file-id options] + (let [file-id (uuid/parse* file-id) + team-id (if-let [id (obj/get options "teamId")] + (uuid/parse* id) + (:current-team-id @st/state))] + (cond + (or (nil? file-id) (nil? team-id)) + (js/Promise.reject (js/Error. "Expected a file UUID and a team UUID")) + + (and (= "ready" (:status (workspace-context @st/state))) + (= file-id (:current-file-id @st/state)) + (= team-id (:current-team-id @st/state))) + (js/Promise.resolve nil) + + :else + (js/Promise. + (fn [resolve reject] + (let [key (js/Symbol) + initial-route (:route @st/state) + timer (atom nil) + finish (fn [error] + (remove-watch st/state key) + (js/clearTimeout @timer) + (if error (reject error) (resolve nil)))] + (reset! timer (js/setTimeout #(finish (js/Error. "Timed out opening file")) 30000)) + (add-watch + st/state key + (fn [_ _ _ state] + (let [workspace (workspace-context state) + target? (and (= (str file-id) (:fileId workspace)) + (= (str team-id) (:team-id (rt/get-params state))))] + (cond + (:exception state) + (finish (js/Error. "Unable to open file")) + + (and target? (= "ready" (:status workspace))) + (finish nil) + + (and (not= initial-route (:route state)) (not target?)) + (finish (js/Error. "File navigation was superseded")))))) + (st/emit! (rt/nav :workspace {:team-id team-id :file-id file-id})))))))) + +(defn create-context + [] + (obj/reify {:name "PenpotManagementContext"} + :workspace + {:get (fn [] (clj->js (workspace-context @st/state)))} + :openFile open-file)) diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs index c70b4f9e7a..fc709427d4 100644 --- a/frontend/src/app/plugins/register.cljs +++ b/frontend/src/app/plugins/register.cljs @@ -102,6 +102,7 @@ {:plugin-id plugin-id :url (str plugin-url) :version vers + :scope (obj/get manifest "scope") :name name :description desc :host origin diff --git a/frontend/test/frontend_tests/data/workspace_mcp_test.cljs b/frontend/test/frontend_tests/data/workspace_mcp_test.cljs index 8bd36d07aa..802be3ae2f 100644 --- a/frontend/test/frontend_tests/data/workspace_mcp_test.cljs +++ b/frontend/test/frontend_tests/data/workspace_mcp_test.cljs @@ -8,14 +8,43 @@ (:require [app.common.time :as ct] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.plugins :as dp] [app.main.data.profile :as du] [app.main.data.workspace.mcp :as mcp] + [app.plugins.register :as preg] [beicon.v2.core :as rx] [cljs.test :as t :include-macros true] [frontend-tests.helpers.async :as a] + [frontend-tests.helpers.mock :as mock] [potok.v2.core :as ptk])) +(t/deftest ^:async test-initialize-once-per-authenticated-session + (let [stream (rx/subject) + events (atom []) + profile {:id (uuid/next)}] + (await + (mock/with-mocks* + {cf/flags #{:mcp} + preg/wait-for-runtime (fn [] (js/Promise.resolve true))} + (let [subscription (rx/subs! #(swap! events conj (ptk/type %)) + (ptk/watch (mcp/initialize) {} stream))] + (try + (rx/push! stream (du/set-profile profile)) + (await (a/wait-for #(= 1 (count @events)))) + (rx/push! stream (du/set-profile profile)) + (rx/push! stream (ptk/data-event :app.main.data.workspace/finalize-workspace)) + (await (a/settle)) + (t/is (= [:app.main.data.workspace.mcp/init] @events)) + (rx/push! stream (ptk/data-event :app.main.data.auth/logged-out)) + (rx/push! stream (du/set-profile profile)) + (await (a/wait-for #(= 2 (count @events)))) + (t/is (= [:app.main.data.workspace.mcp/init + :app.main.data.workspace.mcp/init] @events)) + (finally + (rx/dispose! subscription) + (rx/end! stream)))))))) + (t/deftest test-update-mcp-status (t/testing "enables MCP in profile props and mcp state" (let [state {:profile {:props {:mcp-enabled false}} :mcp {}} @@ -151,7 +180,7 @@ (t/is (= "disconnected" (get-in result [:mcp :connection-status]))) (t/is (nil? (get-in result [:mcp :session-id]))))) -(t/deftest test-init-clears-previous-file-connection +(t/deftest test-init-clears-previous-app-connection (let [state {:profile {:props {:mcp-enabled true}} :mcp {:connection-requested true :connection-status "connected" @@ -165,8 +194,8 @@ (let [state {:mcp {:connection-requested false :connection-status "disconnected"}}] (t/is (= state (ptk/update (mcp/update-mcp-connection-status "connected") state))))) -(t/deftest test-plugin-callbacks-stop-with-workspace - (doseq [stop-event [:app.main.data.workspace/finalize-workspace +(t/deftest test-plugin-callbacks-stop-with-app-session + (doseq [stop-event [:app.main.data.auth/logged-out :app.main.data.workspace.mcp/init]] (let [stream (rx/subject) extension (atom nil) @@ -179,6 +208,8 @@ (.on @extension "connect" #(swap! calls inc)) (rx/push! stream (ptk/data-event :app.main.data.workspace.mcp/connect)) (t/is (= 1 @calls)) + (rx/push! stream (ptk/data-event :app.main.data.workspace/finalize-workspace)) + (t/is (zero? @closed)) (rx/push! stream (ptk/data-event stop-event)) (t/is (= 1 @closed)) (t/is (false? (.isConnectionRequested @extension))) diff --git a/frontend/test/frontend_tests/plugins/management_test.cljs b/frontend/test/frontend_tests/plugins/management_test.cljs new file mode 100644 index 0000000000..1e11a83367 --- /dev/null +++ b/frontend/test/frontend_tests/plugins/management_test.cljs @@ -0,0 +1,83 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.plugins.management-test + (:require + [app.common.uuid :as uuid] + [app.main.store :as st] + [app.plugins.events :as events] + [app.plugins.management :as management] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.async :as a] + [frontend-tests.helpers.mock :as mock])) + +(def file-id (uuid/next)) +(def page-id (uuid/next)) +(def team-id (uuid/next)) + +(def dashboard + {:route {:data {:name :dashboard-recent}} + :current-team-id team-id}) + +(def loading + (assoc dashboard + :route {:data {:name :workspace} + :params {:query {:file-id (str file-id) :team-id (str team-id)}}} + :current-file-id file-id + :workspace-ready file-id)) + +(def ready + (assoc loading + :current-page-id page-id + :files {file-id {:id file-id :name "Design" + :data {:pages-index {page-id {:id page-id :objects {}}}}}})) + +(t/deftest workspace-readiness-requires-an-initialized-page + (t/is (= "none" (:status (management/workspace-context dashboard)))) + (t/is (= "loading" (:status (management/workspace-context loading)))) + (t/is (= "ready" (:status (management/workspace-context ready)))) + (t/is (= "none" (:status (management/workspace-context (assoc ready :route (:route dashboard))))))) + +(t/deftest ^:async open-file-waits-for-page-readiness + (let [state (atom dashboard) + seen (atom []) + done? (atom false)] + (await + (mock/with-mocks* + {st/state state + st/emit! (fn ([_] (js/setTimeout #(reset! state loading) 0)) + ([_ & _] nil))} + (let [listener (events/add-listener "workspacechange" (str uuid/zero) + #(swap! seen conj (.-status %)) nil) + result (-> (management/open-file (str file-id) nil) + (.then #(reset! done? true)))] + (try + (await (a/wait-for #(= ["loading"] @seen))) + (t/is (false? @done?)) + (reset! state ready) + (await result) + (t/is (= ["loading" "ready"] @seen)) + (t/is (true? @done?)) + (finally + (events/remove-listener listener)))))))) + +(t/deftest ^:async open-file-rejects-superseded-navigation + (let [state (atom dashboard)] + (await + (mock/with-mocks* + {st/state state + st/emit! (fn ([_] (js/setTimeout #(reset! state loading) 0)) + ([_ & _] nil))} + (let [result (-> (management/open-file (str file-id) nil) + (.then (fn [] nil) (fn [error] (.-message error))))] + (await (a/wait-for #(= loading @state))) + (reset! state {:route {:data {:name :settings-profile}}}) + (t/is (= "File navigation was superseded" (await result)))))))) + +(t/deftest ^:async open-file-rejects-invalid-identifiers + (let [result (-> (management/open-file "invalid" #js {:teamId (str team-id)}) + (.then (fn [] nil) (fn [error] (.-message error))))] + (t/is (= "Expected a file UUID and a team UUID" (await result))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index a12c63a205..a17d9b3149 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -73,6 +73,7 @@ [frontend-tests.plugins.interactions-test] [frontend-tests.plugins.library-test] [frontend-tests.plugins.local-storage-test] + [frontend-tests.plugins.management-test] [frontend-tests.plugins.page-active-validation-test] [frontend-tests.plugins.page-test] [frontend-tests.plugins.parser-test] @@ -214,6 +215,7 @@ 'frontend-tests.plugins.comments-test 'frontend-tests.plugins.context-shapes-test 'frontend-tests.plugins.events-test + 'frontend-tests.plugins.management-test 'frontend-tests.plugins.file-test 'frontend-tests.plugins.flex-test 'frontend-tests.plugins.format-test diff --git a/mcp/README.md b/mcp/README.md index 1661f0d019..7018537793 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -33,6 +33,16 @@ This repository thus contains not only the MCP server implementation itself but also the supporting Penpot MCP Plugin (see section [Repository Structure](#repository-structure) below). +## Global plugin lifecycle + +- The MCP plugin stays loaded across dashboard and workspace navigation, including when the integration runs with its UI hidden. +- Enabling the integration loads the plugin; connecting remains an explicit action in the current tab. Logout closes it. +- The MCP session ID is a short, 10-character Base32 hash of `penpot.currentUser.sessionId` alone. File metadata changes without replacing the WebSocket connection or execution `storage`. +- `penpotMgmt.workspace` reports `none`, `loading` or `ready`, with file and team metadata. Subscribe through `penpotMgmt.on("workspacechange", callback)`. +- `await penpotMgmt.openFile(fileId, {teamId})` opens a file in the same tab and waits for its page to be ready. The team defaults to the current team; failed navigation rejects the promise. +- The `penpot` facade stays live. File operations require a ready workspace; reacquire file, page and shape references after navigation. +- Global plugins declare `"scope": "global"` in their manifest. Management metadata and navigation currently require `content:read` permission. + ## Demonstration [![Video](https://v32155.1blu.de/penpot/PenpotFest2025_thumbnail.png)](https://v32155.1blu.de/penpot/PenpotFest2025.mp4) @@ -243,7 +253,7 @@ of the prompt input area. Connect the files you want to use: * Open each file in a separate Penpot tab, run the plugin, and connect it to the same MCP server. -* Each connection has its own short session ID, shown in the plugin UI with a **Copy** button. +* Each browser tab uses a short session ID derived only from `penpot.currentUser.sessionId`, shown in the plugin UI with a **Copy** button. The ID stays the same across workspace navigation. * Multiple tabs can connect to the same file. Connecting or disconnecting one tab does not disconnect the others. Choose a session for your agent: diff --git a/mcp/packages/common/src/types.ts b/mcp/packages/common/src/types.ts index 3809a14fdd..f9d7cde400 100644 --- a/mcp/packages/common/src/types.ts +++ b/mcp/packages/common/src/types.ts @@ -1,8 +1,9 @@ /** Metadata identifying one connected Penpot plugin session. */ export interface PenpotSession { sessionId: string; - fileId: string; - fileName: string; + fileId: string | null; + fileName: string | null; + workspaceState?: "none" | "loading" | "ready"; } /** First message sent by the plugin on each new WebSocket connection. */ diff --git a/mcp/packages/plugin/public/manifest.json b/mcp/packages/plugin/public/manifest.json index 022c7ece9c..c67296f1db 100644 --- a/mcp/packages/plugin/public/manifest.json +++ b/mcp/packages/plugin/public/manifest.json @@ -3,6 +3,7 @@ "code": "plugin.js", "icon": "icon.jpg", "version": 2, + "scope": "global", "description": "This plugin enables interaction with the Penpot MCP server", "permissions": ["user:read", "content:read", "content:write", "library:read", "library:write", "comment:read", "comment:write"] } diff --git a/mcp/packages/plugin/src/ConnectionMetadata.test.ts b/mcp/packages/plugin/src/ConnectionMetadata.test.ts index 954eefeb9f..91aadb8188 100644 --- a/mcp/packages/plugin/src/ConnectionMetadata.test.ts +++ b/mcp/packages/plugin/src/ConnectionMetadata.test.ts @@ -92,7 +92,7 @@ test("sends file metadata before marking a plugin connection ready", async () => }; await ui.message(initialization); const registeredSession = ui.sockets[0].sent[0].session; - assert.equal(registeredSession.sessionId, "pq3gxqddgj"); + assert.equal(registeredSession.sessionId, "ftdmhltgwk"); assert.notEqual(registeredSession.sessionId, sessionId); assert.deepEqual(registeredSession, { ...initialization.session, sessionId: registeredSession.sessionId }); assert.equal( @@ -100,7 +100,10 @@ test("sends file metadata before marking a plugin connection ready", async () => false ); ui.sockets[0].receive({ type: "initialized" }); - assert.equal(ui.messages.at(-1).status, "connected"); + assert.equal( + ui.messages.filter((message) => message.type === "update-connection-status").at(-1).status, + "connected" + ); }); test("reconnects with the same session ID and ignores metadata for the old socket", async () => { @@ -163,6 +166,12 @@ test("plugin reads the current file for each connection metadata request", () => runInNewContext(compile("plugin.ts"), { exports: {}, penpot, + penpotMgmt: { + get workspace() { + return { status: "ready", fileId: penpot.currentFile.id, fileName: penpot.currentFile.name }; + }, + on() {}, + }, mcp: undefined, require: () => ({ ExecuteCodeTaskHandler: class {} }), }); @@ -173,17 +182,17 @@ test("plugin reads the current file for each connection metadata request", () => { type: "initialize", penpotUserSessionId: "tab-1", - session: { sessionId: "first", fileId: "file-1", fileName: "First design" }, + session: { sessionId: "first", fileId: "file-1", fileName: "First design", workspaceState: "ready" }, }, { type: "initialize", penpotUserSessionId: "tab-1", - session: { sessionId: "second", fileId: "file-2", fileName: "Second design" }, + session: { sessionId: "second", fileId: "file-2", fileName: "Second design", workspaceState: "ready" }, }, ]); }); -test("integrated connections reuse the short ID across reconnects", async () => { +test("integrated connections reuse the short session ID across reconnects", async () => { const ui = pluginUi(); ui.message({ type: "mcp-mode", integratedRemoteMcp: true }); const initialize = async (requestId: string) => { @@ -194,15 +203,18 @@ test("integrated connections reuse the short ID across reconnects", async () => }); }; await initialize(ui.connect()); - assert.equal(ui.sockets[0].sent[0].session.sessionId, "pq3gxqddgj"); + assert.equal(ui.sockets[0].sent[0].session.sessionId, "ftdmhltgwk"); ui.sockets[0].receive({ type: "initialized" }); - assert.equal(ui.messages.at(-1).sessionId, "pq3gxqddgj"); + assert.equal( + ui.messages.filter((message) => message.type === "update-connection-status").at(-1).sessionId, + "ftdmhltgwk" + ); ui.sockets[0].close(); [...ui.timers.values()][0](); ui.sockets[1].open(); const requestId = ui.messages.filter((message) => message.type === "connection-metadata-request").at(-1).sessionId; await initialize(requestId); - assert.equal(ui.sockets[1].sent[0].session.sessionId, "pq3gxqddgj"); + assert.equal(ui.sockets[1].sent[0].session.sessionId, "ftdmhltgwk"); }); test("disconnect cancels metadata initialization and ignores a late acknowledgement", async () => { @@ -216,8 +228,9 @@ test("disconnect cancels metadata initialization and ignores a late acknowledgem }); ui.message({ type: "stop-server" }); await pending; - ui.sockets[0].receive({ type: "initialized" }); assert.deepEqual(ui.sockets[0].sent, []); + ui.sockets[0].receive({ type: "initialized" }); + assert.ok(!ui.messages.some((message) => message.status === "connected")); assert.equal(ui.messages.at(-1).status, "disconnected"); assert.equal(ui.timers.size, 0); }); @@ -243,6 +256,7 @@ test("integrated plugin stays idle until explicitly connected, including while t }, on() {}, }, + penpotMgmt: { on() {} }, mcp: { getToken: () => "test-token", getServerUrl: () => "ws://localhost:4402", @@ -280,7 +294,10 @@ test("standalone session ID survives reconnects and plugin restarts in the same }); assert.equal(ui.sockets[1].sent[0].session.sessionId, sessionId); ui.sockets[1].receive({ type: "initialized" }); - assert.equal(ui.messages.at(-1).sessionId, sessionId); + assert.equal( + ui.messages.filter((message) => message.type === "update-connection-status").at(-1).sessionId, + sessionId + ); const next = pluginUi(); const nextRequestId = next.connect(); @@ -291,3 +308,37 @@ test("standalone session ID survives reconnects and plugin restarts in the same }); assert.equal(next.sockets[0].sent[0].session.sessionId, sessionId); }); + +test("file metadata does not affect the short session ID", async () => { + for (const fileId of [null, "file-1", "file-2"]) { + const ui = pluginUi(); + const requestId = ui.connect(); + await ui.message({ + type: "initialize", + penpotUserSessionId: "tab-1", + session: { sessionId: requestId, fileId, fileName: fileId }, + }); + assert.equal(ui.sockets[0].sent[0].session.sessionId, "ftdmhltgwk"); + } +}); + +test("dashboard sessions use the short session ID and publish context changes without reconnecting", async () => { + const ui = pluginUi(); + const requestId = ui.connect(); + await ui.message({ + type: "initialize", + penpotUserSessionId: "tab-1", + session: { sessionId: requestId, fileId: null, fileName: null, workspaceState: "none" }, + }); + assert.equal(ui.sockets[0].sent[0].session.sessionId, "ftdmhltgwk"); + ui.sockets[0].receive({ type: "initialized" }); + assert.equal(ui.messages.at(-1).type, "context-request"); + for (const context of [ + { fileId: "file-1", fileName: "Design", workspaceState: "ready" }, + { fileId: null, fileName: null, workspaceState: "none" }, + ]) { + await ui.message({ type: "context-update", context }); + assert.deepEqual(ui.sockets[0].sent.at(-1), { type: "context-update", context }); + } + assert.equal(ui.sockets.length, 1); +}); diff --git a/mcp/packages/plugin/src/SessionId.test.ts b/mcp/packages/plugin/src/SessionId.test.ts index 8837fc7d2b..dbaa3ed162 100644 --- a/mcp/packages/plugin/src/SessionId.test.ts +++ b/mcp/packages/plugin/src/SessionId.test.ts @@ -2,13 +2,13 @@ import assert from "node:assert/strict"; import test from "node:test"; import { SessionId } from "./SessionId.ts"; -test("derives a stable 50-bit Base32 identifier from the Penpot user session and file", async () => { - assert.equal(await SessionId.forFile("tab-1", "file-1"), "pq3gxqddgj"); - assert.equal(await SessionId.forFile("tab-1", "file-1"), "pq3gxqddgj"); - assert.equal(await SessionId.forFile("tab-2", "file-1"), "jablgbun4p"); - assert.equal(await SessionId.forFile("tab-1", "file-2"), "7s3kow4ucx"); +test("derives a stable 50-bit Base32 identifier from only the Penpot user session", async () => { + assert.equal(await SessionId.forSession("tab-1"), "ftdmhltgwk"); + assert.equal(await SessionId.forSession("tab-1"), "ftdmhltgwk"); + assert.equal(await SessionId.forSession("tab-2"), "cb3oexpdmd"); }); -test("encodes the Penpot user session and file without ambiguous boundaries", async () => { - assert.notEqual(await SessionId.forFile("ab", "c"), await SessionId.forFile("a", "bc")); +test("shortens a UUID session to ten copyable Base32 characters", async () => { + const sessionId = await SessionId.forSession("a7457b7c-cf45-80d4-8008-ba5b1d08e6c8"); + assert.match(sessionId, /^[a-z2-7]{10}$/); }); diff --git a/mcp/packages/plugin/src/SessionId.ts b/mcp/packages/plugin/src/SessionId.ts index e81f3e5a59..22e14b4309 100644 --- a/mcp/packages/plugin/src/SessionId.ts +++ b/mcp/packages/plugin/src/SessionId.ts @@ -2,8 +2,8 @@ export class SessionId { private static readonly ALPHABET = "abcdefghijklmnopqrstuvwxyz234567"; - public static async forFile(penpotUserSessionId: string, fileId: string): Promise { - const input = new TextEncoder().encode(JSON.stringify([penpotUserSessionId, fileId])); + public static async forSession(penpotUserSessionId: string): Promise { + const input = new TextEncoder().encode(penpotUserSessionId); const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", input)); return SessionId.encode(digest); } diff --git a/mcp/packages/plugin/src/index.d.ts b/mcp/packages/plugin/src/index.d.ts index 1fa11567bb..67a498275f 100644 --- a/mcp/packages/plugin/src/index.d.ts +++ b/mcp/packages/plugin/src/index.d.ts @@ -8,6 +8,7 @@ interface McpOptions { declare global { const mcp: undefined | McpOptions; + const penpotMgmt: import("../../../../plugins/libs/plugin-types").PenpotMgmt; } export {}; diff --git a/mcp/packages/plugin/src/main.ts b/mcp/packages/plugin/src/main.ts index 29e5c0a286..71973ade0b 100644 --- a/mcp/packages/plugin/src/main.ts +++ b/mcp/packages/plugin/src/main.ts @@ -249,6 +249,7 @@ function connectToMcpServer(baseUrl?: string, token?: string): void { cancelReconnect(); startHeartbeat(); updateConnectionStatus("connected", "Connected"); + parent.postMessage({ type: "context-request" }, "*"); return; } // Track the current task received from the MCP server @@ -349,10 +350,7 @@ window.addEventListener("message", async (event) => { if (!initialization.penpotUserSessionId) { throw new Error("Missing Penpot user session ID"); } - const sessionId = await SessionId.forFile( - initialization.penpotUserSessionId, - initialization.session.fileId - ); + const sessionId = await SessionId.forSession(initialization.penpotUserSessionId); if ( !shouldReconnect || ws !== socket || @@ -377,6 +375,12 @@ window.addEventListener("message", async (event) => { } return; } + if (event.data.type === "context-update") { + if (connectionReady && ws?.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(event.data)); + } + return; + } if (event.data.type === "mcp-mode") { isIntegratedRemoteMcp = event.data.integratedRemoteMcp; } diff --git a/mcp/packages/plugin/src/plugin.ts b/mcp/packages/plugin/src/plugin.ts index eb01d2d571..bb55706a10 100644 --- a/mcp/packages/plugin/src/plugin.ts +++ b/mcp/packages/plugin/src/plugin.ts @@ -57,7 +57,7 @@ penpot.ui.onMessage< penpotVersion: penpotVersionPrefix, }); } - // connect only when requested in this workspace + // connect only when requested in this tab if (isIntegratedRemoteMcp && mcp?.isConnectionRequested()) { penpot.ui.sendMessage({ type: "start-server", @@ -66,15 +66,22 @@ penpot.ui.onMessage< }); } } else if (typeof message === "object" && message.type === "connection-metadata-request") { - const file = penpot.currentFile; - if (file && message.sessionId) { + const workspace = penpotMgmt.workspace; + if (message.sessionId) { const initialization: PluginConnectionInit & { penpotUserSessionId?: string } = { type: "initialize", - session: { sessionId: message.sessionId, fileId: file.id, fileName: file.name }, + session: { + sessionId: message.sessionId, + fileId: workspace.fileId, + fileName: workspace.fileName, + workspaceState: workspace.status, + }, penpotUserSessionId: penpot.currentUser.sessionId, }; penpot.ui.sendMessage(initialization); } + } else if (typeof message === "object" && message.type === "context-request") { + sendWorkspaceContext(); } else if (typeof message === "object" && message.type === "update-connection-status") { mcp?.setMcpStatus(message.status || "unknown", message.sessionId); } else if (typeof message === "object" && message.task && message.id) { @@ -85,6 +92,21 @@ penpot.ui.onMessage< } }); +/** Sends the current workspace metadata without replacing the connection. */ +function sendWorkspaceContext(): void { + const workspace = penpotMgmt.workspace; + penpot.ui.sendMessage({ + type: "context-update", + context: { + fileId: workspace.fileId, + fileName: workspace.fileName, + workspaceState: workspace.status, + }, + }); +} + +penpotMgmt.on("workspacechange", sendWorkspaceContext); + /** * Handles plugin task requests received from the MCP server via WebSocket. * diff --git a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts index 87ea2c7616..2132446f80 100644 --- a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts +++ b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts @@ -178,6 +178,7 @@ export class ExecuteCodeTaskHandler extends TaskHandler { // initialize context, making penpot, penpotUtils, storage and the custom console available this.context = { penpot: penpot, + penpotMgmt: penpotMgmt, storage: {}, console: new ExecuteCodeTaskConsole(), penpotUtils: PenpotUtils, @@ -217,7 +218,7 @@ export class ExecuteCodeTaskHandler extends TaskHandler { // wait for layout updates prior to executing the supplied code (if method is available) try { // @ts-ignore - TODO Penpot.waitForLayoutUpdate is not yet in the released types - if (penpot.waitForLayoutUpdate) { + if (penpotMgmt.workspace.status === "ready" && penpot.waitForLayoutUpdate) { // @ts-ignore await penpot.waitForLayoutUpdate(); } diff --git a/mcp/packages/server/src/PluginBridge.sessions.test.ts b/mcp/packages/server/src/PluginBridge.sessions.test.ts index 1472353588..1a42241169 100644 --- a/mcp/packages/server/src/PluginBridge.sessions.test.ts +++ b/mcp/packages/server/src/PluginBridge.sessions.test.ts @@ -143,3 +143,29 @@ test("invalid initialization metadata cannot register a session", async () => { assert.equal((await closed)[0], 1008); await assert.rejects(bridge.executePluginTask(new PluginTask("test", {})), /No Penpot sessions/); }); + +test("a dashboard session remains routable while its workspace metadata changes", async () => { + const socket = new WebSocket(`ws://127.0.0.1:${port}?userToken=alice`); + sockets.push(socket); + await once(socket, "open"); + const initialized = once(socket, "message"); + socket.send( + JSON.stringify({ + type: "initialize", + session: { sessionId: "tab", fileId: null, fileName: null, workspaceState: "none" }, + }) + ); + await initialized; + socket.on("message", (raw) => { + const request = JSON.parse(raw.toString()); + socket.send(JSON.stringify({ id: request.id, success: true, data: "ok" })); + }); + for (const context of [ + { fileId: "file-1", fileName: "Design", workspaceState: "ready" }, + { fileId: null, fileName: null, workspaceState: "none" }, + ]) { + socket.send(JSON.stringify({ type: "context-update", context })); + assert.equal((await bridge.executePluginTask(new PluginTask("test", {}), "tab")).data, "ok"); + assert.deepEqual(bridge.getUserConnections("alice")!.getSessions(), [{ sessionId: "tab", ...context }]); + } +}); diff --git a/mcp/packages/server/src/PluginBridge.ts b/mcp/packages/server/src/PluginBridge.ts index f53ed3f849..913b28b26a 100644 --- a/mcp/packages/server/src/PluginBridge.ts +++ b/mcp/packages/server/src/PluginBridge.ts @@ -27,8 +27,9 @@ const connectionInitSchema = z.object({ type: z.literal("initialize"), session: z.object({ sessionId: z.string().min(1).max(128), - fileId: z.string().min(1).max(128), - fileName: z.string(), + fileId: z.string().min(1).max(128).nullable(), + fileName: z.string().nullable(), + workspaceState: z.enum(["none", "loading", "ready"]).optional(), }), }); @@ -170,6 +171,13 @@ export class PluginBridge implements TaskDispatchHost { return; } connection.frozen = false; + if (message?.type === "context-update") { + const context = connectionInitSchema.shape.session + .omit({ sessionId: true }) + .parse(message.context); + Object.assign(connection.session, context); + return; + } if (message?.type === "heartbeat") { return; } diff --git a/mcp/packages/server/src/dispatch/TaskDispatcher.ts b/mcp/packages/server/src/dispatch/TaskDispatcher.ts index b9fdcf17aa..062af6b80c 100644 --- a/mcp/packages/server/src/dispatch/TaskDispatcher.ts +++ b/mcp/packages/server/src/dispatch/TaskDispatcher.ts @@ -31,7 +31,7 @@ export abstract class TaskDispatcher { sessions .map( (session) => - `${session.sessionId}: ${JSON.stringify(session.fileName)} (file ${session.fileId})` + `${session.sessionId}: ${session.fileId ? `${JSON.stringify(session.fileName)} (file ${session.fileId})` : "no active workspace"}` ) .join("\n") ); diff --git a/mcp/packages/server/src/tools/ExecuteCodeTool.ts b/mcp/packages/server/src/tools/ExecuteCodeTool.ts index ee15401005..7f913a5700 100644 --- a/mcp/packages/server/src/tools/ExecuteCodeTool.ts +++ b/mcp/packages/server/src/tools/ExecuteCodeTool.ts @@ -49,8 +49,10 @@ export class ExecuteCodeTool extends Tool { "Executes JavaScript code in the Penpot plugin context.\n" + "IMPORTANT: Before using this tool, make sure you have read the 'Penpot High-Level Overview' and know " + "which Penpot API functionality is necessary and how to use it.\n" + - "You have access two main objects: `penpot` (the Penpot API, of type `Penpot`), `penpotUtils`, " + - "and `storage`.\n" + + "You have access to `penpot` (the Penpot API, of type `Penpot`), `penpotMgmt`, `penpotUtils`, and `storage`.\n" + + "`penpotMgmt.workspace.status` is none, loading or ready. Use `await penpotMgmt.openFile(fileId, {teamId})` " + + "to open a file and wait until it is ready; teamId defaults to the current team. " + + "File operations require a ready workspace. Storage survives navigation; reacquire file and shape objects after changing files.\n" + "`storage` is an object in which arbitrary data can be stored, simply by adding a new attribute; " + "stored attributes can be referenced in future calls to this tool, so any intermediate results that " + "could come in handy later should be stored in `storage` instead of just a fleeting variable; " + diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 4b45df68b0..8ff05e595b 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -2,6 +2,8 @@ ### 🚀 Features +- **plugins-runtime**: Added manifest `scope: "global"` for plugins that survive workspace navigation and close on logout. +- **plugin-types**: Added `penpotMgmt` with workspace readiness, `workspacechange` events and `openFile()`, available to global plugins with `content:read` permission. - **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently. - **plugin-types**: Added `waitForLayoutUpdate` to wait until pending layout updates have finished. It rejects when the optional timeout elapses, defaulting to 30 seconds so a wait never hangs. - **plugin-types**: Added `waitForLayoutUpdate` to the `Shape` interface to wait until the pending layout updates of a shape and its children have finished diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index b7dbcc02fc..0ef43fdd4d 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -4,7 +4,7 @@ */ export interface Penpot extends Omit< Context, - 'addListener' | 'removeListener' + 'addListener' | 'removeListener' | 'management' > { readonly ui: { /** @@ -778,10 +778,32 @@ export interface CommonLayout { remove(): void; } +/** Current workspace of this browser tab. */ +export interface WorkspaceContext { + readonly status: 'none' | 'loading' | 'ready'; + readonly fileId: string | null; + readonly fileName: string | null; + readonly teamId: string | null; +} + +/** Application management API available to global plugins. */ +export interface PenpotMgmt { + readonly workspace: WorkspaceContext; + /** Opens a file in this tab and resolves once its workspace and page are ready. */ + openFile(fileId: string, options?: { teamId?: string }): Promise; + /** Subscribes to workspace availability and metadata changes. */ + on( + type: 'workspacechange', + callback: (workspace: WorkspaceContext) => void, + ): symbol; + off(listenerId: symbol): void; +} + /** * Represents the context of Penpot, providing access to various Penpot functionalities and data. */ export interface Context { + readonly management?: Pick; /** * Returns the current penpot version. */ @@ -1556,6 +1578,8 @@ export interface Ellipse extends ShapeBase { * ``` */ export interface EventsMap { + readonly workspacechange: WorkspaceContext; + readonly logout: void; /** * The `pagechange` event is triggered when the active page in the project is changed. */ @@ -5757,4 +5781,5 @@ export interface Viewport { declare global { const penpot: Penpot; + const penpotMgmt: PenpotMgmt; } diff --git a/plugins/libs/plugins-runtime/src/lib/api/index.ts b/plugins/libs/plugins-runtime/src/lib/api/index.ts index 8956a5124e..a0ef062d3e 100644 --- a/plugins/libs/plugins-runtime/src/lib/api/index.ts +++ b/plugins/libs/plugins-runtime/src/lib/api/index.ts @@ -19,6 +19,7 @@ import type { Page, Path, Penpot, + PenpotMgmt, Rectangle, Shape, SvgRaw, @@ -36,6 +37,8 @@ import { createPluginManager } from '../plugin-manager.js'; export const validEvents = [ 'finish', + 'logout', + 'workspacechange', 'pagechange', 'filechange', 'selectionchange', @@ -47,10 +50,24 @@ export const validEvents = [ export function createApi( plugin: Awaited>, ) { - const checkPermission = (permission: Permissions) => { + const workspaceUnavailable = () => + plugin.manifest.scope === 'global' && + plugin.context.management?.workspace.status !== 'ready'; + + const requireWorkspace = () => { + if (workspaceUnavailable()) { + throw new Error( + 'No workspace is ready. Use penpotMgmt.openFile() first.', + ); + } + }; + + const checkPermission = (permission: Permissions, workspace = true) => { if (!plugin.manifest.permissions.includes(permission)) { throw new Error(`Permission ${permission} is not granted`); } + if (workspace && /^(content|library|comment):/.test(permission)) + requireWorkspace(); }; const penpot: Penpot = { @@ -162,7 +179,7 @@ export function createApi( z.function().parse(callback); // To suscribe to events needs the read permission - checkPermission('content:read'); + checkPermission('content:read', false); return plugin.registerListener(type, callback, props); }, @@ -178,18 +195,18 @@ export function createApi( }, get root(): Shape | null { - checkPermission('content:read'); - return plugin.context.root; + checkPermission('content:read', false); + return workspaceUnavailable() ? null : plugin.context.root; }, get currentFile(): File | null { - checkPermission('content:read'); - return plugin.context.currentFile; + checkPermission('content:read', false); + return workspaceUnavailable() ? null : plugin.context.currentFile; }, get currentPage(): Page | null { - checkPermission('content:read'); - return plugin.context.currentPage; + checkPermission('content:read', false); + return workspaceUnavailable() ? null : plugin.context.currentPage; }, get selection(): Shape[] { @@ -203,10 +220,12 @@ export function createApi( }, get viewport(): Viewport { + requireWorkspace(); return plugin.context.viewport; }, get history(): HistoryContext { + requireWorkspace(); return plugin.context.history; }, @@ -395,7 +414,28 @@ export function createApi( }, }; + const penpotMgmt: PenpotMgmt = { + get workspace() { + checkPermission('content:read', false); + return plugin.context.management!.workspace; + }, + openFile(fileId, options) { + checkPermission('content:read', false); + return plugin.context.management!.openFile(fileId, options); + }, + on(type, callback) { + z.literal('workspacechange').parse(type); + z.function().parse(callback); + checkPermission('content:read', false); + return plugin.registerListener(type, callback); + }, + off(listenerId) { + plugin.destroyListener(listenerId); + }, + }; + return { penpot, + penpotMgmt, }; } diff --git a/plugins/libs/plugins-runtime/src/lib/create-sandbox.spec.ts b/plugins/libs/plugins-runtime/src/lib/create-sandbox.spec.ts index 97cc4bb123..45af8b7621 100644 --- a/plugins/libs/plugins-runtime/src/lib/create-sandbox.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/create-sandbox.spec.ts @@ -33,6 +33,7 @@ describe('createSandbox', () => { beforeEach(() => { mockPlugin = { + manifest: { scope: 'workspace' }, code: 'console.log("Plugin running");', timeouts: new Set>(), } as unknown as Awaited>; diff --git a/plugins/libs/plugins-runtime/src/lib/create-sandbox.ts b/plugins/libs/plugins-runtime/src/lib/create-sandbox.ts index 292ae49183..c8aaf3c40a 100644 --- a/plugins/libs/plugins-runtime/src/lib/create-sandbox.ts +++ b/plugins/libs/plugins-runtime/src/lib/create-sandbox.ts @@ -1,4 +1,3 @@ -import type { Penpot } from '@penpot/plugin-types'; import type { createPluginManager } from './plugin-manager'; import { createApi } from './api'; import { ses } from './ses.js'; @@ -67,7 +66,7 @@ export function createSandbox( const pluginApi = createApi(plugin); const safeHandler = { - get(target: Penpot, prop: string, receiver: unknown) { + get(target: object, prop: string, receiver: unknown) { const originalValue = Reflect.get(target, prop, receiver); if (typeof originalValue === 'function') { @@ -202,6 +201,12 @@ export function createSandbox( publicPluginApi = Object.assign(publicPluginApi, apiExtensions); } + if (plugin.manifest.scope === 'global') { + Object.assign(publicPluginApi, { + penpotMgmt: new Proxy(pluginApi.penpotMgmt, safeHandler), + }); + } + const compartment = ses.createCompartment(publicPluginApi); return { diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts index 06ef7b20b7..4151e09fea 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts @@ -98,9 +98,14 @@ describe('loadPlugin real initialization path (regression for #11001)', () => { ); // The plugin code really ran inside the sandbox: the manager registers - // `themechange` + `finish`, and the plugin code adds its own `finish` + // `themechange` + `finish` + `logout`, and the plugin code adds its own `finish` // listener through the public API. - expect(fixture.listenerTypes).toEqual(['themechange', 'finish', 'finish']); + expect(fixture.listenerTypes).toEqual([ + 'themechange', + 'finish', + 'logout', + 'finish', + ]); expect(getPlugins()).toHaveLength(1); // The user-facing behavior from #11001: host-side augmentation of a @@ -112,7 +117,7 @@ describe('loadPlugin real initialization path (regression for #11001)', () => { expect(() => { freshWrapper.toString = () => 'patched-by-runtime'; }).not.toThrow(); - expect(fixture.listenerTypes.length).toBe(3); + expect(fixture.listenerTypes.length).toBe(4); }); it('denies the write API without permission and leaves the host untouched', async () => { @@ -166,4 +171,79 @@ describe('loadPlugin real initialization path (regression for #11001)', () => { expect(fixture.createRectangle).toHaveBeenCalledTimes(1); expect(lastCompartmentGlobalThis()['__selectionFrozen']).toBe(true); }); + it('keeps a global sandbox through navigation and other plugin loads, then closes it on logout', async () => { + const fixture = makeHostFixture(); + let workspace = { + status: 'none', + fileId: null, + fileName: null, + teamId: null, + }; + const listeners = new Map< + symbol, + { type: string; callback: (value?: unknown) => void } + >(); + const context = { + ...fixture.context, + management: { + get workspace() { + return workspace; + }, + openFile: async () => {}, + }, + addListener(type: string, callback: (value?: unknown) => void) { + const id = Symbol(); + listeners.set(id, { type, callback }); + return id; + }, + removeListener(id: symbol) { + listeners.delete(id); + }, + } as unknown as Context; + setContextBuilder(() => context); + const manifest = { + ...makeManifest( + 'globalThis.savedPenpot = penpot; globalThis.probe = penpotMgmt.workspace.status;', + ['content:read', 'content:write'], + ), + pluginId: 'global-plugin', + scope: 'global' as const, + }; + await loadPlugin(manifest); + const globalPlugin = getPlugins().find( + (plugin) => plugin.manifest.pluginId === 'global-plugin', + )!; + const globals = globalPlugin.compartment.compartment.globalThis; + expect(globals['probe']).toBe('none'); + expect(() => + globalPlugin.compartment.compartment.evaluate('penpot.createRectangle()'), + ).toThrow(/No workspace/); + for (const listener of [...listeners.values()]) { + if (listener.type === 'finish') listener.callback(); + } + await loadPlugin(makeManifest('globalThis.workspacePlugin = true;', [])); + expect(getPlugins()).toContain(globalPlugin); + await loadPlugin(manifest); + expect( + getPlugins().filter( + (plugin) => plugin.manifest.pluginId === 'global-plugin', + ), + ).toHaveLength(1); + workspace = { ...workspace, status: 'ready' }; + globalPlugin.compartment.compartment.evaluate( + 'penpot.createRectangle(); globalThis.sameFacade = savedPenpot === penpot;', + ); + expect(globals['sameFacade']).toBe(true); + expect(fixture.createRectangle).toHaveBeenCalledOnce(); + await expect( + loadPlugin(makeManifest('throw new Error("failure");', [])), + ).rejects.toThrow('failure'); + expect(getPlugins()).toContain(globalPlugin); + for (const listener of [...listeners.values()]) { + if (listener.type === 'logout') listener.callback(); + } + expect(getPlugins()).toHaveLength(0); + expect(globals['penpotMgmt']).toBeUndefined(); + expect(listeners.size).toBe(0); + }); }); diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts index e22e62c70c..be332d9058 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts @@ -17,20 +17,19 @@ export function setContextBuilder(builder: ContextBuilder) { export const getPlugins = () => plugins; const closeAllPlugins = () => { - // Background plugins keep running, so they must stay registered: the - // registry routes their UI messages and lets them be unloaded later. - const backgroundPlugins: typeof plugins = []; - - plugins.forEach((pluginApi) => { + // Background and global plugins keep running, so they must stay registered: + // the registry routes their UI messages and lets them be unloaded later. + plugins = plugins.filter((pluginApi) => { /* eslint-disable @typescript-eslint/no-explicit-any */ - if ((pluginApi.manifest as any)?.allowBackground) { - backgroundPlugins.push(pluginApi); - } else { - pluginApi.plugin.close(); + if ( + pluginApi.manifest?.scope === 'global' || + (pluginApi.manifest as any)?.allowBackground + ) { + return true; } + pluginApi.plugin.close(); + return false; }); - - plugins = backgroundPlugins; }; window.addEventListener('message', (event) => { @@ -57,7 +56,17 @@ export const loadPlugin = async function ( return; } - closeAllPlugins(); + if (manifest.scope === 'global') { + if ( + plugins.some( + (plugin) => plugin.manifest?.pluginId === manifest.pluginId, + ) + ) { + return; + } + } else { + closeAllPlugins(); + } // The host context is not deeply frozen at this load stage. // @@ -74,7 +83,8 @@ export const loadPlugin = async function ( // `createSandbox`'s proxy handler applies `ses.safeReturn` to values // crossing into the sandbox. Compartment isolation and intrinsics // hardening are performed by createSandbox, not here. - const plugin = await createPlugin( + let plugin: Awaited> | undefined = undefined; + plugin = await createPlugin( context, manifest, () => { @@ -88,7 +98,7 @@ export const loadPlugin = async function ( ); plugins.push(plugin); } catch (error) { - closeAllPlugins(); + if (manifest.scope !== 'global') closeAllPlugins(); throw error; } }; diff --git a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts index 16d4fd5e28..aefc563c65 100644 --- a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts +++ b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts @@ -7,6 +7,7 @@ export const manifestSchema = z.object({ code: z.string(), icon: z.string().optional(), version: z.number().optional(), + scope: z.enum(['workspace', 'global']).optional(), description: z.string().max(200).optional(), permissions: z.array( z.enum([ diff --git a/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts b/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts index 7878a1abcd..f430321210 100644 --- a/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts +++ b/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts @@ -40,11 +40,10 @@ export async function createPluginManager( modal?.setTheme(theme); }); - const listenerId: symbol = context.addListener('finish', () => { - closePlugin(); - - context?.removeListener(listenerId); + const listenerId = context.addListener('finish', () => { + if (manifest.scope !== 'global') closePlugin(); }); + const logoutId = context.addListener('logout', () => closePlugin()); let listeners: symbol[] = []; @@ -60,7 +59,11 @@ export async function createPluginManager( }; const closePlugin = () => { + if (destroyed) return; + destroyed = true; removeAllEventListeners(); + destroyListener(listenerId); + destroyListener(logoutId); timeouts.forEach(clearTimeout); timeouts.clear(); @@ -74,8 +77,6 @@ export async function createPluginManager( modal = null; } - destroyed = true; - onCloseCallback(); }; From 54cce66c13e569a5870d6bb922b6e6a09bf2a267 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 2 Oct 2026 13:19:11 +0000 Subject: [PATCH 063/145] :sparkles: Share the MCP connection menu with the dashboard Connection status: - Replace the dashboard's action-only link with an explicit connected label and indicator, using the theme's existing accent colour. - Reflect the actual connection state independently of connection intent. Shared controls: - Extract the toolbar's MCP menu and styles into a shared component used by both the dashboard and workspace. - Provide connection status, session ID, copy, connect and disconnect actions in both locations, preserving hover and keyboard behaviour. - Keep dashboard and toolbar event origins distinct and update the dashboard connection instructions. Validation: - Confirm dashboard disconnect/reconnect, menu opening, keyboard navigation and Escape dismissal in the running app. - Pass live compilation, targeted ClojureScript and SCSS lint, formatting checks and git diff checks. AI-assisted-by: gpt-6 Signed-off-by: Dominik Jain --- docs/mcp/index.md | 2 +- .../src/app/main/ui/components/mcp_menu.cljs | 141 ++++++++++++++++++ .../src/app/main/ui/components/mcp_menu.scss | 127 ++++++++++++++++ .../src/app/main/ui/dashboard/sidebar.cljs | 22 +-- .../app/main/ui/workspace/top_toolbar.cljs | 120 +-------------- .../app/main/ui/workspace/top_toolbar.scss | 106 ------------- 6 files changed, 276 insertions(+), 242 deletions(-) create mode 100644 frontend/src/app/main/ui/components/mcp_menu.cljs create mode 100644 frontend/src/app/main/ui/components/mcp_menu.scss diff --git a/docs/mcp/index.md b/docs/mcp/index.md index 47a4c55803..cce812b516 100644 --- a/docs/mcp/index.md +++ b/docs/mcp/index.md @@ -81,7 +81,7 @@ Some important concepts for users: Connect the tabs you want to use: -* **Remote MCP**: choose **MCP: Connect** in the dashboard sidebar or **MCP → Connect** in a file's toolbar. Use **Disconnect** in the same place to disconnect that tab. +* **Remote MCP**: choose **MCP → Connect** in the dashboard sidebar or a file's toolbar. Use **Disconnect** in the same place to disconnect that tab. * **Local MCP**: run the plugin in each file and connect it to your local server. Each plugin connection has its own session ID. * Connecting or disconnecting one tab does not disconnect the others. * A connected tab stays connected while navigating between the dashboard and files. Its session ID stays the same; requests operate on the currently open file. diff --git a/frontend/src/app/main/ui/components/mcp_menu.cljs b/frontend/src/app/main/ui/components/mcp_menu.cljs new file mode 100644 index 0000000000..f38e4f377b --- /dev/null +++ b/frontend/src/app/main/ui/components/mcp_menu.cljs @@ -0,0 +1,141 @@ +;; 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 SUBSIDIARY SL + +(ns app.main.ui.components.mcp-menu + (:require-macros [app.main.style :as stl]) + (:require + [app.main.data.event :as ev] + [app.main.data.notifications :as ntf] + [app.main.data.workspace.mcp :as mcp] + [app.main.store :as st] + [app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.util.clipboard :as clipboard] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [app.util.timers :as ts] + [rumext.v2 :as mf])) + +(defn- cancel-timer! + [timer-ref*] + (when-let [timer (mf/ref-val timer-ref*)] + (ts/dispose! timer) + (mf/set-ref-val! timer-ref* nil))) + +(mf/defc mcp-menu* + {::mf/wrap [mf/memo]} + [{:keys [is-mcp-connected is-connection-requested session-id dashboard]}] + (let [copied-text (tr "workspace.toolbar.mcp-session-copied") + copy-error (tr "errors.clipboard-api-unavailable") + menu-open* (mf/use-state false) + menu-open? (deref menu-open*) + + open-timer* (mf/use-ref nil) + close-timer* (mf/use-ref nil) + + on-open-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (cancel-timer! open-timer*) + (cancel-timer! close-timer*) + (reset! menu-open* true))) + + on-close-menu + (mf/use-fn + (fn [] + (cancel-timer! open-timer*) + (cancel-timer! close-timer*) + (reset! menu-open* false))) + + on-display-menu + (mf/use-fn + (fn [] + (cancel-timer! close-timer*) + (cancel-timer! open-timer*) + (mf/set-ref-val! + open-timer* + (ts/schedule 350 + #(do + (reset! menu-open* true) + (mf/set-ref-val! open-timer* nil)))))) + + on-hide-menu + (mf/use-fn + (fn [] + (cancel-timer! open-timer*) + (cancel-timer! close-timer*) + (mf/set-ref-val! + close-timer* + (ts/schedule 350 + #(do + (reset! menu-open* false) + (mf/set-ref-val! close-timer* nil)))))) + + on-connect + (mf/use-fn + (mf/deps dashboard) + #(st/emit! (mcp/connect-mcp) + (ev/event {::ev/name "connect-mcp-plugin" + ::ev/origin (if dashboard "dashboard:sidebar" "workspace:toolbar")}))) + + on-disconnect + (mf/use-fn + #(st/emit! (mcp/user-disconnect-mcp))) + + on-copy-session + (mf/use-fn + (mf/deps session-id copied-text copy-error) + (fn [] + (-> (clipboard/to-clipboard session-id) + (.then #(st/emit! (ntf/info copied-text))) + (.catch #(st/emit! (ntf/error copy-error))))))] + + (mf/with-effect [] + (fn [] + (cancel-timer! open-timer*) + (cancel-timer! close-timer*))) + + [:div {:class (stl/css-case :mcp-tool true :dashboard dashboard) + :on-pointer-enter on-display-menu + :on-pointer-leave on-hide-menu} + [:> button* {:variant "ghost" + :on-click on-open-menu + :aria-haspopup true + :aria-expanded menu-open? + :aria-pressed menu-open? + :data-tool "mcp" + :data-testid "mcp-btn"} + [:div {:class (stl/css-case :toolbar-mcp-button true + :selected menu-open?)} + [:span {:class (stl/css-case :toolbar-mcp-button-dot true + :connected is-mcp-connected)}] + [:span {:class (stl/css-case :toolbar-mcp-button-label true + :connected is-mcp-connected)} + (if (and dashboard is-mcp-connected) + (tr "workspace.toolbar.mcp-connected") + (tr "workspace.toolbar.mcp"))]]] + + [:div {:class (stl/css :toolbar-mcp-menu)} + [:> dropdown-menu* {:show menu-open? + :on-close on-close-menu + :class (stl/css :toolbar-mcp-dropdown)} + (when (or is-mcp-connected session-id) + [:li {:class (stl/css :toolbar-mcp-dropdown-info) + :role "presentation"} + (when is-mcp-connected + [:span (tr "workspace.toolbar.mcp-connected")]) + (when session-id + [:span (tr "workspace.toolbar.mcp-session-id" session-id)])]) + (when session-id + [:> dropdown-menu-item* {:class (stl/css :toolbar-mcp-dropdown-item) + :on-click on-copy-session} + (tr "workspace.toolbar.mcp-copy-session-id")]) + [:> dropdown-menu-item* {:class (stl/css :toolbar-mcp-dropdown-item) + :on-click (if is-connection-requested on-disconnect on-connect)} + (if is-connection-requested + (tr "workspace.header.menu.mcp.plugin.status.disconnect") + (tr "workspace.header.menu.mcp.plugin.status.connect"))]]]])) diff --git a/frontend/src/app/main/ui/components/mcp_menu.scss b/frontend/src/app/main/ui/components/mcp_menu.scss new file mode 100644 index 0000000000..f4c69ba4c8 --- /dev/null +++ b/frontend/src/app/main/ui/components/mcp_menu.scss @@ -0,0 +1,127 @@ +// 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 SUBSIDIARY SL + +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/typography.scss" as t; +@use "ds/z-index.scss" as *; + +.mcp-tool { + position: relative; +} + +.dashboard { + margin-inline: var(--sp-l); +} + +.toolbar-mcp-button { + display: flex; + align-items: center; + gap: var(--sp-xs); +} + +.toolbar-mcp-button-label { + &.connected { + color: var(--color-accent-primary); + } +} + +.toolbar-mcp-button-dot { + position: relative; + display: flex; + flex-shrink: 0; + inline-size: px2rem(6); + block-size: px2rem(6); + border-radius: 50%; + background-color: var(--color-background-disabled); + + &.connected { + background-color: var(--color-accent-primary); + } + + // One-shot "blob" ripple that confirms the moment the tab becomes + // connected (e.g. after using "Switch here"). Triggered purely by the + // `.connected` class appearing, in sync with the color change. + &.connected::after { + content: ""; + position: absolute; + inset: 0; + border-radius: 50%; + background-color: var(--color-accent-primary); + opacity: 0; + animation: mcp-status-blob 0.5s ease-out; + } +} + +@keyframes mcp-status-blob { + from { + opacity: 0.5; + transform: scale(1); + } + + to { + opacity: 0; + transform: scale(2.5); + } +} + +.toolbar-mcp-menu { + position: absolute; + left: 0; + top: $sz-36; +} + +// Same look as the workspace main menu (`.base-menu` in main_menu.scss). +// The toolbar overrides `--menu-background-color`, so restore the global +// menu value here. +.toolbar-mcp-dropdown { + --menu-background-color: var(--color-background-tertiary); + + display: flex; + flex-direction: column; + gap: var(--sp-xs); + z-index: var(--z-index-dropdown); + margin: 0; + padding: var(--sp-xs); + border: $b-2 solid var(--panel-border-color); + border-radius: $br-8; + background-color: var(--menu-background-color); + box-shadow: 0 0 $sz-12 0 var(--menu-shadow-color); +} + +.toolbar-mcp-dropdown-info { + @include t.use-typography("body-small"); + + display: flex; + flex-direction: column; + gap: var(--sp-xs); + padding: var(--sp-s) var(--sp-m); + border-radius: $br-8; + color: var(--color-foreground-secondary); + background-color: var(--color-background-primary); + font-size: px2rem(11); + white-space: nowrap; +} + +.toolbar-mcp-dropdown-item { + @include t.use-typography("body-small"); + + cursor: pointer; + display: flex; + align-items: center; + padding: var(--sp-s) var(--sp-m); + border-radius: $br-8; + color: var(--menu-foreground-color); + background-color: var(--menu-background-color); + white-space: nowrap; + + &:hover { + --menu-foreground-color: var(--menu-foreground-color-hover); + --menu-background-color: var(--menu-background-color-hover); + } +} diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 386d1dc791..8787c290ca 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -17,13 +17,13 @@ [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] [app.main.data.team :as dtm] - [app.main.data.workspace.mcp :as mcp] [app.main.refs :as refs] [app.main.router :as rt] [app.main.store :as st] [app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]] [app.main.ui.components.link :refer [link*]] + [app.main.ui.components.mcp-menu :refer [mcp-menu*]] [app.main.ui.dashboard.check-updates :as dcu] [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] @@ -257,12 +257,6 @@ team-id (get team :id) mcp-state (mf/deref refs/mcp) - mcp-requested? (:connection-requested mcp-state) - toggle-mcp (mf/use-fn - (mf/deps mcp-requested?) - #(st/emit! (if mcp-requested? - (mcp/user-disconnect-mcp) - (mcp/connect-mcp)))) projects? (= section :dashboard-recent) fonts? (= section :dashboard-fonts) @@ -394,16 +388,10 @@ (:enabled mcp-state) (:token-valid mcp-state)) [:div {:class (stl/css :sidebar-content-section)} - [:ul {:class (stl/css :sidebar-nav)} - [:li {:class (stl/css :sidebar-nav-item)} - [:> link* {:action toggle-mcp - :keyboard-action toggle-mcp - :class (stl/css :sidebar-link)} - [:span {:class (stl/css :element-title)} - "MCP: " - (if mcp-requested? - (tr "workspace.header.menu.mcp.plugin.status.disconnect") - (tr "workspace.header.menu.mcp.plugin.status.connect"))]]]]]) + [:> mcp-menu* {:dashboard true + :is-mcp-connected (= "connected" (:connection-status mcp-state)) + :is-connection-requested (:connection-requested mcp-state) + :session-id (:session-id mcp-state)}]]) [:div {:class (stl/css :sidebar-content-section) :data-testid "pinned-projects"} diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.cljs b/frontend/src/app/main/ui/workspace/top_toolbar.cljs index 0a10cf095d..7a7f767621 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/top_toolbar.cljs @@ -12,24 +12,20 @@ [app.config :as cf] [app.main.data.event :as ev] [app.main.data.modal :as modal] - [app.main.data.notifications :as ntf] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing.common :as dwdc] - [app.main.data.workspace.mcp :as mcp] [app.main.data.workspace.media :as dwm] [app.main.data.workspace.path.state :as pst] [app.main.data.workspace.shortcuts :as sc] [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]] [app.main.ui.components.file-uploader :as file-uploader] + [app.main.ui.components.mcp-menu :refer [mcp-menu*]] [app.main.ui.context :as ctx] - [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :as i] - [app.util.clipboard :as clipboard] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -297,118 +293,6 @@ :ref ref :on-selected on-selected}]])) -(mf/defc mcp-tool* - {::mf/private true - ::mf/wrap [mf/memo]} - [{:keys [is-mcp-connected is-connection-requested session-id]}] - (let [copied-text (tr "workspace.toolbar.mcp-session-copied") - copy-error (tr "errors.clipboard-api-unavailable") - menu-open* (mf/use-state false) - menu-open? (deref menu-open*) - - open-timer* (mf/use-ref nil) - close-timer* (mf/use-ref nil) - - on-open-menu - (mf/use-fn - (fn [event] - (dom/stop-propagation event) - (cancel-timer! open-timer*) - (cancel-timer! close-timer*) - (reset! menu-open* true))) - - on-close-menu - (mf/use-fn - (fn [] - (cancel-timer! open-timer*) - (cancel-timer! close-timer*) - (reset! menu-open* false))) - - on-display-menu - (mf/use-fn - (fn [] - (cancel-timer! close-timer*) - (cancel-timer! open-timer*) - (mf/set-ref-val! - open-timer* - (ts/schedule 350 - #(do - (reset! menu-open* true) - (mf/set-ref-val! open-timer* nil)))))) - - on-hide-menu - (mf/use-fn - (fn [] - (cancel-timer! open-timer*) - (cancel-timer! close-timer*) - (mf/set-ref-val! - close-timer* - (ts/schedule 350 - #(do - (reset! menu-open* false) - (mf/set-ref-val! close-timer* nil)))))) - - on-connect - (mf/use-fn - #(st/emit! (mcp/connect-mcp) - (ev/event {::ev/name "connect-mcp-plugin" - ::ev/origin "workspace:toolbar"}))) - - on-disconnect - (mf/use-fn - #(st/emit! (mcp/user-disconnect-mcp))) - - on-copy-session - (mf/use-fn - (mf/deps session-id copied-text copy-error) - (fn [] - (-> (clipboard/to-clipboard session-id) - (.then #(st/emit! (ntf/info copied-text))) - (.catch #(st/emit! (ntf/error copy-error))))))] - - (mf/with-effect [] - (fn [] - (cancel-timer! open-timer*) - (cancel-timer! close-timer*))) - - [:div {:on-pointer-enter on-display-menu - :on-pointer-leave on-hide-menu} - [:> button* {:variant "ghost" - :on-click on-open-menu - :aria-haspopup true - :aria-expanded menu-open? - :aria-pressed menu-open? - :data-tool "mcp" - :data-testid "mcp-btn"} - [:div {:class (stl/css-case :toolbar-mcp-button true - :selected menu-open?)} - [:span {:class (stl/css-case :toolbar-mcp-button-dot true - :connected is-mcp-connected)}] - [:span {:class (stl/css-case :toolbar-mcp-button-label true - :connected is-mcp-connected)} - (tr "workspace.toolbar.mcp")]]] - - [:div {:class (stl/css :toolbar-mcp-menu)} - [:> dropdown-menu* {:show menu-open? - :on-close on-close-menu - :class (stl/css :toolbar-mcp-dropdown)} - (when (or is-mcp-connected session-id) - [:li {:class (stl/css :toolbar-mcp-dropdown-info) - :role "presentation"} - (when is-mcp-connected - [:span (tr "workspace.toolbar.mcp-connected")]) - (when session-id - [:span (tr "workspace.toolbar.mcp-session-id" session-id)])]) - (when session-id - [:> dropdown-menu-item* {:class (stl/css :toolbar-mcp-dropdown-item) - :on-click on-copy-session} - (tr "workspace.toolbar.mcp-copy-session-id")]) - [:> dropdown-menu-item* {:class (stl/css :toolbar-mcp-dropdown-item) - :on-click (if is-connection-requested on-disconnect on-connect)} - (if is-connection-requested - (tr "workspace.header.menu.mcp.plugin.status.disconnect") - (tr "workspace.header.menu.mcp.plugin.status.connect"))]]]])) - (mf/defc top-toolbar* {::mf/wrap [mf/memo]} [{:keys [layout]}] @@ -545,7 +429,7 @@ (when mcp-show? [:li {:class (stl/css :toolbar-option)} - [:> mcp-tool* {:is-mcp-connected mcp-connected? + [:> mcp-menu* {:is-mcp-connected mcp-connected? :is-connection-requested (:connection-requested mcp) :session-id (:session-id mcp)}]])] diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.scss b/frontend/src/app/main/ui/workspace/top_toolbar.scss index 7f5637c8f5..71ad771b3c 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.scss +++ b/frontend/src/app/main/ui/workspace/top_toolbar.scss @@ -101,112 +101,6 @@ } } -.toolbar-mcp-button { - display: flex; - align-items: center; - gap: var(--sp-xs); -} - -.toolbar-mcp-button-label { - &.connected { - color: var(--color-accent-primary); - } -} - -.toolbar-mcp-button-dot { - position: relative; - display: flex; - flex-shrink: 0; - inline-size: px2rem(6); - block-size: px2rem(6); - border-radius: 50%; - background-color: var(--color-background-disabled); - - &.connected { - background-color: var(--color-accent-primary); - } - - // One-shot "blob" ripple that confirms the moment the tab becomes - // connected (e.g. after using "Switch here"). Triggered purely by the - // `.connected` class appearing, in sync with the color change. - &.connected::after { - content: ""; - position: absolute; - inset: 0; - border-radius: 50%; - background-color: var(--color-accent-primary); - opacity: 0; - animation: mcp-status-blob 0.5s ease-out; - } -} - -@keyframes mcp-status-blob { - from { - opacity: 0.5; - transform: scale(1); - } - - to { - opacity: 0; - transform: scale(2.5); - } -} - -.toolbar-mcp-menu { - position: absolute; - left: 0; - top: $sz-36; -} - -// Same look as the workspace main menu (`.base-menu` in main_menu.scss). -// The toolbar overrides `--menu-background-color`, so restore the global -// menu value here. -.toolbar-mcp-dropdown { - --menu-background-color: var(--color-background-tertiary); - - display: flex; - flex-direction: column; - gap: var(--sp-xs); - z-index: var(--z-index-dropdown); - margin: 0; - padding: var(--sp-xs); - border: $b-2 solid var(--panel-border-color); - border-radius: $br-8; - background-color: var(--menu-background-color); - box-shadow: 0 0 $sz-12 0 var(--menu-shadow-color); -} - -.toolbar-mcp-dropdown-info { - @include t.use-typography("body-small"); - - display: flex; - flex-direction: column; - gap: var(--sp-xs); - padding: var(--sp-s) var(--sp-m); - border-radius: $br-8; - color: var(--color-foreground-secondary); - background-color: var(--color-background-primary); - font-size: px2rem(11); - white-space: nowrap; -} - -.toolbar-mcp-dropdown-item { - @include t.use-typography("body-small"); - - display: flex; - align-items: center; - padding: var(--sp-s) var(--sp-m); - border-radius: $br-8; - color: var(--menu-foreground-color); - background-color: var(--menu-background-color); - white-space: nowrap; - - &:hover { - --menu-foreground-color: var(--menu-foreground-color-hover); - --menu-background-color: var(--menu-background-color-hover); - } -} - .toolbar-separator { --separator-color: var(--color-background-quaternary); From 2bb6cd557eff7b2c457a96a42e43da7d9ee4f8c3 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 2 Oct 2026 14:32:56 +0000 Subject: [PATCH 064/145] :sparkles: Add project and file discovery to penpotMgmt Discovery: - Add listProjects and project-scoped listFiles with content:read checks. - Return metadata from existing RPCs in dashboard and workspace contexts. - Reject invalid IDs and preserve backend access errors. Documentation: - Keep usage in API type comments and include regenerated MCP API docs. - Remove README implementation details and consolidate the changelog. - Add usage information to the MCP server's initial instructions Validation: - Pass 8 frontend tests, 81 runtime tests and the runtime build. - Verify both APIs through live MCP calls in dashboard and workspace. - Pass type, lint, formatting and diff checks. AI-assisted-by: gpt-6 Signed-off-by: Dominik Jain --- frontend/src/app/plugins/management.cljs | 48 ++- .../plugins/management_test.cljs | 71 +++++ mcp/README.md | 10 - mcp/packages/server/data/api_types.yml | 273 +++++++++++++++++- .../server/data/initial_instructions.md | 10 +- .../server/src/tools/ExecuteCodeTool.ts | 5 +- plugins/CHANGELOG.md | 3 +- plugins/libs/plugin-types/index.d.ts | 73 ++++- .../libs/plugins-runtime/src/lib/api/index.ts | 8 + .../src/lib/api/plugin-api.spec.ts | 54 ++++ 10 files changed, 532 insertions(+), 23 deletions(-) diff --git a/frontend/src/app/plugins/management.cljs b/frontend/src/app/plugins/management.cljs index bff5d9292d..9dadac543b 100644 --- a/frontend/src/app/plugins/management.cljs +++ b/frontend/src/app/plugins/management.cljs @@ -8,9 +8,11 @@ (:require [app.common.uuid :as uuid] [app.main.data.helpers :as dsh] + [app.main.repo :as rp] [app.main.router :as rt] [app.main.store :as st] - [app.util.object :as obj])) + [app.util.object :as obj] + [beicon.v2.core :as rx])) (defn workspace-context [state] @@ -70,9 +72,51 @@ (finish (js/Error. "File navigation was superseded")))))) (st/emit! (rt/nav :workspace {:team-id team-id :file-id file-id})))))))) +(defn list-projects + [options] + (let [team-id (if-let [id (obj/get options "teamId")] + (uuid/parse* id) + (:current-team-id @st/state))] + (if (nil? team-id) + (js/Promise.reject (js/Error. "Expected a team UUID")) + (js/Promise. + (fn [resolve reject] + (->> (rp/cmd! :get-projects {:team-id team-id}) + (rx/map (fn [projects] + (clj->js + (mapv (fn [project] + {:id (str (:id project)) + :teamId (str (:team-id project)) + :name (:name project) + :isDefault (boolean (:is-default project)) + :fileCount (:count project)}) + (remove :deleted-at projects))))) + (rx/subs! resolve reject))))))) + +(defn list-files + [options] + (let [project-id (uuid/parse* (obj/get options "projectId"))] + (if (nil? project-id) + (js/Promise.reject (js/Error. "Expected a project UUID")) + (js/Promise. + (fn [resolve reject] + (->> (rp/cmd! :get-project-files {:project-id project-id}) + (rx/map (fn [files] + (clj->js + (mapv (fn [file] + {:id (str (:id file)) + :teamId (str (:team-id file)) + :projectId (str (:project-id file)) + :name (:name file) + :modifiedAt (.toISOString ^js (:modified-at file))}) + files)))) + (rx/subs! resolve reject))))))) + (defn create-context [] (obj/reify {:name "PenpotManagementContext"} :workspace {:get (fn [] (clj->js (workspace-context @st/state)))} - :openFile open-file)) + :openFile open-file + :listProjects list-projects + :listFiles list-files)) diff --git a/frontend/test/frontend_tests/plugins/management_test.cljs b/frontend/test/frontend_tests/plugins/management_test.cljs index 1e11a83367..9db0ae80c0 100644 --- a/frontend/test/frontend_tests/plugins/management_test.cljs +++ b/frontend/test/frontend_tests/plugins/management_test.cljs @@ -7,9 +7,11 @@ (ns frontend-tests.plugins.management-test (:require [app.common.uuid :as uuid] + [app.main.repo :as rp] [app.main.store :as st] [app.plugins.events :as events] [app.plugins.management :as management] + [beicon.v2.core :as rx] [cljs.test :as t :include-macros true] [frontend-tests.helpers.async :as a] [frontend-tests.helpers.mock :as mock])) @@ -81,3 +83,72 @@ (let [result (-> (management/open-file "invalid" #js {:teamId (str team-id)}) (.then (fn [] nil) (fn [error] (.-message error))))] (t/is (= "Expected a file UUID and a team UUID" (await result))))) + +(t/deftest ^:async list-projects-returns-active-metadata + (let [project-id (uuid/next) + other-team-id (uuid/next)] + (doseq [[state options expected-team-id] + [[dashboard nil team-id] + [ready #js {:teamId (str other-team-id)} other-team-id]]] + (let [state* (atom state)] + (await + (mock/with-mocks* + {st/state state* + rp/cmd! (mock/stub + (fn [command params] + (t/is (= [:get-projects {:team-id expected-team-id}] + [command params])) + (rx/of [{:id project-id :team-id expected-team-id + :name "Drafts" :is-default true :count 2 :total-count 3} + {:id (uuid/next) :deleted-at (js/Date.)}])))} + (let [projects (await (management/list-projects options))] + (t/is (= [{"id" (str project-id) "teamId" (str expected-team-id) + "name" "Drafts" "isDefault" true "fileCount" 2}] + (js->clj projects))) + (t/is (= state @state*))))))))) + +(t/deftest ^:async list-files-returns-metadata-without-opening-a-file + (let [project-id (uuid/next)] + (doseq [state [dashboard ready]] + (let [state* (atom state)] + (await + (mock/with-mocks* + {st/state state* + rp/cmd! (mock/stub + (fn [command params] + (t/is (= [:get-project-files {:project-id project-id}] + [command params])) + (rx/of [{:id file-id :team-id team-id :project-id project-id + :name "Design" :modified-at (js/Date. "2026-01-02T03:04:05Z") + :revn 10}])))} + (let [files (await (management/list-files #js {:projectId (str project-id)}))] + (t/is (= [{"id" (str file-id) "teamId" (str team-id) + "projectId" (str project-id) "name" "Design" + "modifiedAt" "2026-01-02T03:04:05.000Z"}] + (js->clj files))) + (t/is (= state @state*))))))))) + +(t/deftest ^:async listing-rejects-invalid-identifiers-without-querying + (await + (mock/with-mocks* + {st/state (atom {}) + rp/cmd! (mock/stub (fn [_ _] (t/is false "Invalid IDs must not query the backend")))} + (doseq [[result message] + [[(management/list-projects nil) "Expected a team UUID"] + [(management/list-projects #js {:teamId "invalid"}) "Expected a team UUID"] + [(management/list-files nil) "Expected a project UUID"] + [(management/list-files #js {:projectId "invalid"}) "Expected a project UUID"]]] + (t/is (= message (await (.then result (fn [] nil) #(.-message %))))))))) + +(t/deftest ^:async listing-preserves-empty-results-and-backend-errors + (doseq [list-items [(fn [] (management/list-projects #js {:teamId (str team-id)})) + (fn [] (management/list-files #js {:projectId (str (uuid/next))}))]] + (await + (mock/with-mocks* + {rp/cmd! (mock/stub (fn [_ _] (rx/of [])))} + (t/is (= [] (js->clj (await (list-items))))))) + (let [error (js/Error. "Access denied")] + (await + (mock/with-mocks* + {rp/cmd! (mock/stub (fn [_ _] (rx/throw error)))} + (t/is (identical? error (await (.then (list-items) (fn [] nil) identity))))))))) diff --git a/mcp/README.md b/mcp/README.md index 7018537793..59d60aaf61 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -33,16 +33,6 @@ This repository thus contains not only the MCP server implementation itself but also the supporting Penpot MCP Plugin (see section [Repository Structure](#repository-structure) below). -## Global plugin lifecycle - -- The MCP plugin stays loaded across dashboard and workspace navigation, including when the integration runs with its UI hidden. -- Enabling the integration loads the plugin; connecting remains an explicit action in the current tab. Logout closes it. -- The MCP session ID is a short, 10-character Base32 hash of `penpot.currentUser.sessionId` alone. File metadata changes without replacing the WebSocket connection or execution `storage`. -- `penpotMgmt.workspace` reports `none`, `loading` or `ready`, with file and team metadata. Subscribe through `penpotMgmt.on("workspacechange", callback)`. -- `await penpotMgmt.openFile(fileId, {teamId})` opens a file in the same tab and waits for its page to be ready. The team defaults to the current team; failed navigation rejects the promise. -- The `penpot` facade stays live. File operations require a ready workspace; reacquire file, page and shape references after navigation. -- Global plugins declare `"scope": "global"` in their manifest. Management metadata and navigation currently require `content:read` permission. - ## Demonstration [![Video](https://v32155.1blu.de/penpot/PenpotFest2025_thumbnail.png)](https://v32155.1blu.de/penpot/PenpotFest2025.mp4) diff --git a/mcp/packages/server/data/api_types.yml b/mcp/packages/server/data/api_types.yml index e0ebcde698..c926147938 100644 --- a/mcp/packages/server/data/api_types.yml +++ b/mcp/packages/server/data/api_types.yml @@ -92,7 +92,7 @@ Penpot: Hierarchy - * Omit + * Omit + Penpot members: Properties: @@ -5001,6 +5001,250 @@ CommonLayout: The `remove` method removes the layout. + Returns void +WorkspaceContext: + overview: |- + Interface WorkspaceContext + ========================== + + Current workspace of this browser tab. + + ``` + interface WorkspaceContext { + status: "none" | "loading" | "ready"; + fileId: string | null; + fileName: string | null; + teamId: string | null; + } + ``` + + Referenced by: EventsMap, PenpotMgmt + members: + Properties: + status: |- + ``` + readonly status: "none" | "loading" | "ready" + ``` + fileId: |- + ``` + readonly fileId: string | null + ``` + fileName: |- + ``` + readonly fileName: string | null + ``` + teamId: |- + ``` + readonly teamId: string | null + ``` +ProjectSummary: + overview: |- + Interface ProjectSummary + ======================== + + Metadata snapshot of an accessible, non-deleted project. + + ``` + interface ProjectSummary { + id: string; + teamId: string; + name: string; + isDefault: boolean; + fileCount: number; + } + ``` + + Referenced by: PenpotMgmt + members: + Properties: + id: |- + ``` + readonly id: string + ``` + teamId: |- + ``` + readonly teamId: string + ``` + name: |- + ``` + readonly name: string + ``` + isDefault: |- + ``` + readonly isDefault: boolean + ``` + + Whether this is the team's Drafts project. + fileCount: |- + ``` + readonly fileCount: number + ``` +FileSummary: + overview: |- + Interface FileSummary + ===================== + + Metadata snapshot of an accessible, non-deleted file. + + ``` + interface FileSummary { + id: string; + teamId: string; + projectId: string; + name: string; + modifiedAt: string; + } + ``` + + Referenced by: PenpotMgmt + members: + Properties: + id: |- + ``` + readonly id: string + ``` + teamId: |- + ``` + readonly teamId: string + ``` + projectId: |- + ``` + readonly projectId: string + ``` + name: |- + ``` + readonly name: string + ``` + modifiedAt: |- + ``` + readonly modifiedAt: string + ``` + + Last modification time as an ISO 8601 string in UTC. +PenpotMgmt: + overview: |- + Interface PenpotMgmt + ==================== + + Application management API exposed as `penpotMgmt` to global plugins. + + * Requires `content:read` permission. + * Available in the dashboard and file workspaces. + * Use this API to discover and open files; file operations on `penpot` require a ready workspace. + * Reacquire file, page and shape objects after changing files. + + ``` + interface PenpotMgmt { + workspace: WorkspaceContext; + listProjects(options?: { teamId?: string }): Promise; + listFiles(options: { projectId: string }): Promise; + openFile(fileId: string, options?: { teamId?: string }): Promise; + on( + type: "workspacechange", + callback: (workspace: WorkspaceContext) => void, + ): symbol; + off(listenerId: symbol): void; + } + ``` + + Referenced by: Context + members: + Properties: + workspace: |- + ``` + readonly workspace: WorkspaceContext + ``` + + Current workspace availability and metadata; usable for file operations when status is `ready`. + Methods: + listProjects: |- + ``` + listProjects(options?: { teamId?: string }): Promise + ``` + + Lists accessible, non-deleted projects as metadata snapshots. + + * `teamId` defaults to the current team. + * Queries the backend without opening a file or changing navigation. + * Rejects invalid identifiers and backend access errors. + + Parameters + + * options: { teamId?: string } + + Returns Promise + + Example + ``` + const projects = await penpotMgmt.listProjects(); + ``` + listFiles: |- + ``` + listFiles(options: { projectId: string }): Promise + ``` + + Lists accessible, non-deleted files in the specified project as metadata snapshots. + + * Requires an explicit `projectId`. + * Queries the backend without opening a file or changing navigation. + * Rejects invalid identifiers and backend access errors. + + Parameters + + * options: { projectId: string } + + Returns Promise + + Example + ``` + const files = await penpotMgmt.listFiles({ projectId: project.id }); + ``` + openFile: |- + ``` + openFile(fileId: string, options?: { teamId?: string }): Promise + ``` + + Opens a file in this tab and resolves once its workspace and page are ready. + + * `teamId` defaults to the current team. + * Rejects invalid identifiers, failed or superseded navigation, and waits exceeding 30 seconds. + + Parameters + + * fileId: string + * options: { teamId?: string } + + Returns Promise + + Example + ``` + await penpotMgmt.openFile(file.id, { teamId: file.teamId }); + ``` + on: |- + ``` + on( + type: "workspacechange", + callback: (workspace: WorkspaceContext) => void, + ): symbol + ``` + + Subscribes to workspace availability and metadata changes. + + Parameters + + * type: "workspacechange" + * callback: (workspace: WorkspaceContext) => void + + Returns symbol + off: |- + ``` + off(listenerId: symbol): void + ``` + + Parameters + + * listenerId: symbol + Returns void Context: overview: |- @@ -5011,6 +5255,13 @@ Context: ``` interface Context { + management?: Pick< + PenpotMgmt, + "workspace" + | "openFile" + | "listProjects" + | "listFiles", + >; version: string; root: Shape | null; currentFile: File | null; @@ -5082,6 +5333,16 @@ Context: ``` members: Properties: + management: |- + ``` + readonly management?: Pick< + PenpotMgmt, + "workspace" + | "openFile" + | "listProjects" + | "listFiles", + > + ``` version: |- ``` readonly version: string @@ -7109,6 +7370,8 @@ EventsMap: ``` interface EventsMap { + workspacechange: WorkspaceContext; + logout: void; pagechange: Page; filechange: File; selectionchange: string[]; @@ -7122,6 +7385,14 @@ EventsMap: Referenced by: Context, Penpot members: Properties: + workspacechange: |- + ``` + readonly workspacechange: WorkspaceContext + ``` + logout: |- + ``` + readonly logout: void + ``` pagechange: |- ``` readonly pagechange: Page diff --git a/mcp/packages/server/data/initial_instructions.md b/mcp/packages/server/data/initial_instructions.md index be9177619c..ea5961c605 100644 --- a/mcp/packages/server/data/initial_instructions.md +++ b/mcp/packages/server/data/initial_instructions.md @@ -166,13 +166,15 @@ Boards can have layout systems that automatically control the positioning and sp # The `penpot` and `penpotUtils` Objects, Exploring Designs -A key object to use in your code is the `penpot` object (which is of type `Penpot`): +A key object for navigating the currently active Penpot file is the `penpot` object (which is of type `Penpot`): * `penpot.selection` provides the list of shapes the user has selected in the Penpot UI. If it is unclear which elements to work on, you can ask the user to select them for you. ALWAYS immediately copy the selected shape(s) into `storage`! Do not assume that the selection remains unchanged. * `penpot.root` provides the root shape of the currently active page. * Generation of CSS content for elements via `penpot.generateStyle` * Generation of HTML/SVG content for elements via `penpot.generateMarkup` +Members of `penpot` (e.g. `penpot.currentFile` and `penpot.root`) will be null no Penpot file is currently opened. +You can use `penpotMgmt` to navigate to a file if needed (see below). For example, to generate CSS for the currently selected elements, you can execute this: return penpot.generateStyle(penpot.selection, { type: "css", withChildren: true }); @@ -430,5 +432,11 @@ For many tasks, it can be critical to visually inspect the design. Remember to u * The use of flex layouts is encouraged for cases where elements are arranged in rows or columns with consistent spacing/positioning. Consider converting boards to flex layout when appropriate. +# The `penpotMgmt` Object + +This object (of type `PenpotMgmt`) serves to manage Penpot design projects and files. +It allows you to discover and navigate to different design files the user has access to. +You navigate to different design files only when your task explicitly requires it/you are instructed to do so. + -- You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again. diff --git a/mcp/packages/server/src/tools/ExecuteCodeTool.ts b/mcp/packages/server/src/tools/ExecuteCodeTool.ts index 7f913a5700..7a8e710e33 100644 --- a/mcp/packages/server/src/tools/ExecuteCodeTool.ts +++ b/mcp/packages/server/src/tools/ExecuteCodeTool.ts @@ -49,10 +49,7 @@ export class ExecuteCodeTool extends Tool { "Executes JavaScript code in the Penpot plugin context.\n" + "IMPORTANT: Before using this tool, make sure you have read the 'Penpot High-Level Overview' and know " + "which Penpot API functionality is necessary and how to use it.\n" + - "You have access to `penpot` (the Penpot API, of type `Penpot`), `penpotMgmt`, `penpotUtils`, and `storage`.\n" + - "`penpotMgmt.workspace.status` is none, loading or ready. Use `await penpotMgmt.openFile(fileId, {teamId})` " + - "to open a file and wait until it is ready; teamId defaults to the current team. " + - "File operations require a ready workspace. Storage survives navigation; reacquire file and shape objects after changing files.\n" + + "You have access to `penpot` (of type `Penpot`), `penpotMgmt` (of type `PenpotMgmt`), `penpotUtils`, and `storage`.\n" + "`storage` is an object in which arbitrary data can be stored, simply by adding a new attribute; " + "stored attributes can be referenced in future calls to this tool, so any intermediate results that " + "could come in handy later should be stored in `storage` instead of just a fleeting variable; " + diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 8ff05e595b..6dabccbb2f 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -2,8 +2,7 @@ ### 🚀 Features -- **plugins-runtime**: Added manifest `scope: "global"` for plugins that survive workspace navigation and close on logout. -- **plugin-types**: Added `penpotMgmt` with workspace readiness, `workspacechange` events and `openFile()`, available to global plugins with `content:read` permission. +- **plugins-runtime, plugin-types**: Added global plugins (`scope: "global"`) that remain active across dashboard and workspace navigation until logout, with `penpotMgmt` for project and file discovery, file navigation and workspace events. - **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently. - **plugin-types**: Added `waitForLayoutUpdate` to wait until pending layout updates have finished. It rejects when the optional timeout elapses, defaulting to 30 seconds so a wait never hangs. - **plugin-types**: Added `waitForLayoutUpdate` to the `Shape` interface to wait until the pending layout updates of a shape and its children have finished diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index 0ef43fdd4d..d1d145fe96 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -786,10 +786,74 @@ export interface WorkspaceContext { readonly teamId: string | null; } -/** Application management API available to global plugins. */ +/** Metadata snapshot of an accessible, non-deleted project. */ +export interface ProjectSummary { + readonly id: string; + readonly teamId: string; + readonly name: string; + /** Whether this is the team's Drafts project. */ + readonly isDefault: boolean; + readonly fileCount: number; +} + +/** Metadata snapshot of an accessible, non-deleted file. */ +export interface FileSummary { + readonly id: string; + readonly teamId: string; + readonly projectId: string; + readonly name: string; + /** Last modification time as an ISO 8601 string in UTC. */ + readonly modifiedAt: string; +} + +/** + * Application management API exposed as `penpotMgmt` to global plugins. + * + * - Requires `content:read` permission. + * - Available in the dashboard and file workspaces. + * - Use this API to discover and open files; file operations on `penpot` require a ready workspace. + * - Reacquire file, page and shape objects after changing files. + */ export interface PenpotMgmt { + /** Current workspace availability and metadata; usable for file operations when status is `ready`. */ readonly workspace: WorkspaceContext; - /** Opens a file in this tab and resolves once its workspace and page are ready. */ + /** + * Lists accessible, non-deleted projects as metadata snapshots. + * + * - `teamId` defaults to the current team. + * - Queries the backend without opening a file or changing navigation. + * - Rejects invalid identifiers and backend access errors. + * + * @example + * ```ts + * const projects = await penpotMgmt.listProjects(); + * ``` + */ + listProjects(options?: { teamId?: string }): Promise; + /** + * Lists accessible, non-deleted files in the specified project as metadata snapshots. + * + * - Requires an explicit `projectId`. + * - Queries the backend without opening a file or changing navigation. + * - Rejects invalid identifiers and backend access errors. + * + * @example + * ```ts + * const files = await penpotMgmt.listFiles({ projectId: project.id }); + * ``` + */ + listFiles(options: { projectId: string }): Promise; + /** + * Opens a file in this tab and resolves once its workspace and page are ready. + * + * - `teamId` defaults to the current team. + * - Rejects invalid identifiers, failed or superseded navigation, and waits exceeding 30 seconds. + * + * @example + * ```ts + * await penpotMgmt.openFile(file.id, { teamId: file.teamId }); + * ``` + */ openFile(fileId: string, options?: { teamId?: string }): Promise; /** Subscribes to workspace availability and metadata changes. */ on( @@ -803,7 +867,10 @@ export interface PenpotMgmt { * Represents the context of Penpot, providing access to various Penpot functionalities and data. */ export interface Context { - readonly management?: Pick; + readonly management?: Pick< + PenpotMgmt, + 'workspace' | 'openFile' | 'listProjects' | 'listFiles' + >; /** * Returns the current penpot version. */ diff --git a/plugins/libs/plugins-runtime/src/lib/api/index.ts b/plugins/libs/plugins-runtime/src/lib/api/index.ts index a0ef062d3e..ba36b1e12a 100644 --- a/plugins/libs/plugins-runtime/src/lib/api/index.ts +++ b/plugins/libs/plugins-runtime/src/lib/api/index.ts @@ -419,6 +419,14 @@ export function createApi( checkPermission('content:read', false); return plugin.context.management!.workspace; }, + listProjects(options) { + checkPermission('content:read', false); + return plugin.context.management!.listProjects(options); + }, + listFiles(options) { + checkPermission('content:read', false); + return plugin.context.management!.listFiles(options); + }, openFile(fileId, options) { checkPermission('content:read', false); return plugin.context.management!.openFile(fileId, options); diff --git a/plugins/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts b/plugins/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts index 308fb81245..3513b97acd 100644 --- a/plugins/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts @@ -54,6 +54,60 @@ describe('Plugin api', () => { vi.clearAllMocks(); }); + describe('management listing', () => { + it.each(['none', 'loading', 'ready'])( + 'lists metadata with workspace status %s', + async (status) => { + const projects = [{ id: 'project-id' }]; + const files = [{ id: 'file-id' }]; + const management = { + workspace: { status }, + listProjects: vi.fn().mockResolvedValue(projects), + listFiles: vi.fn().mockResolvedValue(files), + }; + const { penpotMgmt } = createApi({ + ...pluginManager, + manifest: { ...pluginManager.manifest, scope: 'global' }, + context: { ...pluginManager.context, management }, + } as any); + + await expect(penpotMgmt.listProjects()).resolves.toEqual(projects); + expect(management.listProjects).toHaveBeenLastCalledWith(undefined); + await penpotMgmt.listProjects({ teamId: 'team-id' }); + expect(management.listProjects).toHaveBeenLastCalledWith({ + teamId: 'team-id', + }); + await expect( + penpotMgmt.listFiles({ projectId: 'project-id' }), + ).resolves.toEqual(files); + expect(management.listFiles).toHaveBeenCalledWith({ + projectId: 'project-id', + }); + }, + ); + + it('requires content:read before querying metadata', () => { + const management = { + listProjects: vi.fn(), + listFiles: vi.fn(), + }; + const { penpotMgmt } = createApi({ + ...pluginManager, + manifest: { + ...pluginManager.manifest, + permissions: [], + scope: 'global', + }, + context: { ...pluginManager.context, management }, + } as any); + + expect(() => penpotMgmt.listProjects()).toThrow(); + expect(() => penpotMgmt.listFiles({ projectId: 'project-id' })).toThrow(); + expect(management.listProjects).not.toHaveBeenCalled(); + expect(management.listFiles).not.toHaveBeenCalled(); + }); + }); + describe('ui', () => { describe.concurrent('permissions', () => { const api = createApi({ From 036f59266bca54831fb565f98024bb7f97889490 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 2 Oct 2026 17:58:52 +0000 Subject: [PATCH 065/145] :bug: Discard cancelled plugin loads after unloading Cause: - Disabling MCP during script loading could leave a late plugin instance attached to a stopped controller, blocking connection after re-enabling. Fix: - Track pending loads and discard instances cancelled by unload. - Prevent duplicate global loads and preserve newer replacement instances. - Clear pending state after failures so a subsequent load can retry. Validation: - Pass 84 runtime tests, the runtime build, lint and formatting checks. - Reproduce the race with a delayed script in the browser, then verify re-enabling restores the session ID, themed indicator and MCP execution. AI-assisted-by: gpt-6 Signed-off-by: Dominik Jain --- plugins/CHANGELOG.md | 2 +- .../src/lib/load-plugin-real-path.spec.ts | 73 ++++++++++++++++++- .../plugins-runtime/src/lib/load-plugin.ts | 16 +++- 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 6dabccbb2f..94cd3f268c 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -2,7 +2,7 @@ ### 🚀 Features -- **plugins-runtime, plugin-types**: Added global plugins (`scope: "global"`) that remain active across dashboard and workspace navigation until logout, with `penpotMgmt` for project and file discovery, file navigation and workspace events. +- **plugins-runtime, plugin-types**: Added global plugins (`scope: "global"`) that remain active across dashboard and workspace navigation until closed or logged out, with `penpotMgmt` for project and file discovery, file navigation and workspace events. - **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently. - **plugin-types**: Added `waitForLayoutUpdate` to wait until pending layout updates have finished. It rejects when the optional timeout elapses, defaulting to 30 seconds so a wait never hangs. - **plugin-types**: Added `waitForLayoutUpdate` to the `Shape` interface to wait until the pending layout updates of a shape and its children have finished diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts index 4151e09fea..2200f434b1 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts @@ -1,6 +1,11 @@ import { describe, it, vi, expect, beforeAll } from 'vitest'; import 'ses'; -import { loadPlugin, setContextBuilder, getPlugins } from './load-plugin'; +import { + loadPlugin, + setContextBuilder, + getPlugins, + ɵunloadPlugin, +} from './load-plugin'; import type { Context } from '@penpot/plugin-types'; import type { Manifest } from './models/manifest.model.js'; @@ -69,7 +74,7 @@ function makeHostFixture() { // public penpot API. Plugin code must never see it (see B-2 below). __internalSecret: 'host-internal', } as unknown as Context; - return { context, listenerTypes, createRectangle, selection }; + return { context, listeners, listenerTypes, createRectangle, selection }; } function lastCompartmentGlobalThis(): Record { @@ -171,6 +176,70 @@ describe('loadPlugin real initialization path (regression for #11001)', () => { expect(fixture.createRectangle).toHaveBeenCalledTimes(1); expect(lastCompartmentGlobalThis()['__selectionFrozen']).toBe(true); }); + it.each(['before', 'after'])( + 'discards a cancelled global load that completes %s its replacement', + async (order) => { + const fixture = makeHostFixture(); + setContextBuilder(() => fixture.context); + const manifest = { + ...makeManifest('plugin.js', []), + host: 'https://plugins.test/', + scope: 'global' as const, + }; + ɵunloadPlugin(manifest.pluginId); + let finishFetch!: (response: object) => void; + const response = new Promise((resolve) => { + finishFetch = resolve; + }); + const fetch = vi.fn().mockReturnValue(response); + vi.stubGlobal('fetch', fetch); + try { + const cancelled = loadPlugin(manifest); + await loadPlugin(manifest); + expect(fetch).toHaveBeenCalledOnce(); + ɵunloadPlugin(manifest.pluginId); + const replacement = () => + loadPlugin({ + ...manifest, + host: '', + code: 'globalThis.marker = "replacement";', + }); + if (order === 'after') await replacement(); + finishFetch({ + ok: true, + text: async () => 'globalThis.marker = "cancelled";', + }); + await cancelled; + if (order === 'before') { + expect(getPlugins()).toHaveLength(0); + expect(fixture.listeners.size).toBe(0); + await replacement(); + } + expect(getPlugins()).toHaveLength(1); + expect(lastCompartmentGlobalThis()['marker']).toBe('replacement'); + expect(fixture.listeners.size).toBe(3); + } finally { + ɵunloadPlugin(manifest.pluginId); + vi.unstubAllGlobals(); + } + }, + ); + + it('allows retrying a global plugin after a failed load', async () => { + const fixture = makeHostFixture(); + setContextBuilder(() => fixture.context); + const manifest = { + ...makeManifest('throw new Error("load failed");', []), + scope: 'global' as const, + }; + await expect(loadPlugin(manifest)).rejects.toThrow('load failed'); + expect(getPlugins()).toHaveLength(0); + await loadPlugin({ ...manifest, code: 'globalThis.marker = "retry";' }); + expect(lastCompartmentGlobalThis()['marker']).toBe('retry'); + ɵunloadPlugin(manifest.pluginId); + expect(fixture.listeners.size).toBe(0); + }); + it('keeps a global sandbox through navigation and other plugin loads, then closes it on logout', async () => { const fixture = makeHostFixture(); let workspace = { diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts index be332d9058..bad0d4fd63 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts @@ -5,6 +5,7 @@ import { Manifest } from './models/manifest.model.js'; import { createPlugin } from './create-plugin.js'; let plugins: Awaited>[] = []; +const pendingPlugins = new Map(); export type ContextBuilder = (id: string) => Context; @@ -49,6 +50,7 @@ export const loadPlugin = async function ( closeCallback?: () => void, apiExtensions?: object, ) { + const loadId = Symbol(); try { const context = contextBuilder && contextBuilder(manifest.pluginId); @@ -58,6 +60,7 @@ export const loadPlugin = async function ( if (manifest.scope === 'global') { if ( + pendingPlugins.has(manifest.pluginId) || plugins.some( (plugin) => plugin.manifest?.pluginId === manifest.pluginId, ) @@ -83,7 +86,9 @@ export const loadPlugin = async function ( // `createSandbox`'s proxy handler applies `ses.safeReturn` to values // crossing into the sandbox. Compartment isolation and intrinsics // hardening are performed by createSandbox, not here. - let plugin: Awaited> | undefined = undefined; + pendingPlugins.set(manifest.pluginId, loadId); + let plugin: Awaited> | undefined = + undefined; plugin = await createPlugin( context, manifest, @@ -96,10 +101,18 @@ export const loadPlugin = async function ( }, apiExtensions, ); + if (pendingPlugins.get(manifest.pluginId) !== loadId) { + plugin.plugin.close(); + return; + } plugins.push(plugin); } catch (error) { if (manifest.scope !== 'global') closeAllPlugins(); throw error; + } finally { + if (pendingPlugins.get(manifest.pluginId) === loadId) { + pendingPlugins.delete(manifest.pluginId); + } } }; @@ -117,6 +130,7 @@ export const ɵloadPluginByUrl = async function (manifestUrl: string) { }; export const ɵunloadPlugin = function (id: Manifest['pluginId']) { + pendingPlugins.delete(id); const plugin = plugins.find((plugin) => plugin.manifest.pluginId === id); if (plugin) { From c4d2ca9cd10fba6635f6d51e66b95b22539ab318 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 7 Oct 2026 13:23:29 +0200 Subject: [PATCH 066/145] :sparkles: Move the dashboard MCP menu to the page headers Show the MCP menu next to the layout toggle in the header of the projects, files and deleted pages, instead of in the sidebar. It uses the secondary button style there and a visible dot colour while disconnected. Clicking the MCP button now toggles the menu instead of only opening it. Dashboard events report "dashboard:header" as their origin. --- docs/mcp/index.md | 2 +- .../src/app/main/ui/components/mcp_menu.cljs | 27 +++++++++++++------ .../src/app/main/ui/components/mcp_menu.scss | 10 +++---- .../src/app/main/ui/dashboard/deleted.cljs | 2 ++ frontend/src/app/main/ui/dashboard/files.cljs | 2 ++ .../src/app/main/ui/dashboard/projects.cljs | 2 ++ .../src/app/main/ui/dashboard/sidebar.cljs | 11 -------- 7 files changed, 31 insertions(+), 25 deletions(-) diff --git a/docs/mcp/index.md b/docs/mcp/index.md index cce812b516..fb9c927106 100644 --- a/docs/mcp/index.md +++ b/docs/mcp/index.md @@ -81,7 +81,7 @@ Some important concepts for users: Connect the tabs you want to use: -* **Remote MCP**: choose **MCP → Connect** in the dashboard sidebar or a file's toolbar. Use **Disconnect** in the same place to disconnect that tab. +* **Remote MCP**: choose **MCP → Connect** in the dashboard header or a file's toolbar. Use **Disconnect** in the same place to disconnect that tab. * **Local MCP**: run the plugin in each file and connect it to your local server. Each plugin connection has its own session ID. * Connecting or disconnecting one tab does not disconnect the others. * A connected tab stays connected while navigating between the dashboard and files. Its session ID stays the same; requests operate on the currently open file. diff --git a/frontend/src/app/main/ui/components/mcp_menu.cljs b/frontend/src/app/main/ui/components/mcp_menu.cljs index f38e4f377b..8e5091bfd3 100644 --- a/frontend/src/app/main/ui/components/mcp_menu.cljs +++ b/frontend/src/app/main/ui/components/mcp_menu.cljs @@ -7,9 +7,11 @@ (ns app.main.ui.components.mcp-menu (:require-macros [app.main.style :as stl]) (:require + [app.config :as cf] [app.main.data.event :as ev] [app.main.data.notifications :as ntf] [app.main.data.workspace.mcp :as mcp] + [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]] [app.main.ui.ds.buttons.button :refer [button*]] @@ -36,13 +38,13 @@ open-timer* (mf/use-ref nil) close-timer* (mf/use-ref nil) - on-open-menu + on-toggle-menu (mf/use-fn (fn [event] (dom/stop-propagation event) (cancel-timer! open-timer*) (cancel-timer! close-timer*) - (reset! menu-open* true))) + (swap! menu-open* not))) on-close-menu (mf/use-fn @@ -80,7 +82,7 @@ (mf/deps dashboard) #(st/emit! (mcp/connect-mcp) (ev/event {::ev/name "connect-mcp-plugin" - ::ev/origin (if dashboard "dashboard:sidebar" "workspace:toolbar")}))) + ::ev/origin (if dashboard "dashboard:header" "workspace:toolbar")}))) on-disconnect (mf/use-fn @@ -102,8 +104,8 @@ [:div {:class (stl/css-case :mcp-tool true :dashboard dashboard) :on-pointer-enter on-display-menu :on-pointer-leave on-hide-menu} - [:> button* {:variant "ghost" - :on-click on-open-menu + [:> button* {:variant (if dashboard "secondary" "ghost") + :on-click on-toggle-menu :aria-haspopup true :aria-expanded menu-open? :aria-pressed menu-open? @@ -115,9 +117,7 @@ :connected is-mcp-connected)}] [:span {:class (stl/css-case :toolbar-mcp-button-label true :connected is-mcp-connected)} - (if (and dashboard is-mcp-connected) - (tr "workspace.toolbar.mcp-connected") - (tr "workspace.toolbar.mcp"))]]] + (tr "workspace.toolbar.mcp")]]] [:div {:class (stl/css :toolbar-mcp-menu)} [:> dropdown-menu* {:show menu-open? @@ -139,3 +139,14 @@ (if is-connection-requested (tr "workspace.header.menu.mcp.plugin.status.disconnect") (tr "workspace.header.menu.mcp.plugin.status.connect"))]]]])) + +(mf/defc dashboard-mcp-menu* + [] + (let [mcp-state (mf/deref refs/mcp)] + (when (and (contains? cf/flags :mcp) + (:enabled mcp-state) + (:token-valid mcp-state)) + [:> mcp-menu* {:dashboard true + :is-mcp-connected (= "connected" (:connection-status mcp-state)) + :is-connection-requested (:connection-requested mcp-state) + :session-id (:session-id mcp-state)}]))) diff --git a/frontend/src/app/main/ui/components/mcp_menu.scss b/frontend/src/app/main/ui/components/mcp_menu.scss index f4c69ba4c8..fd466d4206 100644 --- a/frontend/src/app/main/ui/components/mcp_menu.scss +++ b/frontend/src/app/main/ui/components/mcp_menu.scss @@ -13,10 +13,10 @@ .mcp-tool { position: relative; -} -.dashboard { - margin-inline: var(--sp-l); + &.dashboard { + --mcp-disconnected-dot-color: var(--color-foreground-secondary); + } } .toolbar-mcp-button { @@ -38,7 +38,7 @@ inline-size: px2rem(6); block-size: px2rem(6); border-radius: 50%; - background-color: var(--color-background-disabled); + background-color: var(--mcp-disconnected-dot-color, var(--color-background-disabled)); &.connected { background-color: var(--color-accent-primary); @@ -72,6 +72,7 @@ .toolbar-mcp-menu { position: absolute; + z-index: var(--z-index-dropdown); left: 0; top: $sz-36; } @@ -85,7 +86,6 @@ display: flex; flex-direction: column; gap: var(--sp-xs); - z-index: var(--z-index-dropdown); margin: 0; padding: var(--sp-xs); border: $b-2 solid var(--panel-border-color); diff --git a/frontend/src/app/main/ui/dashboard/deleted.cljs b/frontend/src/app/main/ui/dashboard/deleted.cljs index 2f41c028e6..6bef519d98 100644 --- a/frontend/src/app/main/ui/dashboard/deleted.cljs +++ b/frontend/src/app/main/ui/dashboard/deleted.cljs @@ -16,6 +16,7 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.context-menu-a11y :refer [context-menu*]] + [app.main.ui.components.mcp-menu :refer [dashboard-mcp-menu*]] [app.main.ui.dashboard.grid :refer [grid*]] [app.main.ui.dashboard.layout-toggle :as lt :refer [layout-toggle*]] [app.main.ui.dashboard.subscription :refer [get-subscription-type]] @@ -64,6 +65,7 @@ [:div#dashboard-deleted-title {:class (stl/css :dashboard-title)} [:h1 (tr "dashboard.projects-title")]] [:div {:class (stl/css :dashboard-header-actions)} + [:> dashboard-mcp-menu*] [:> layout-toggle* {:layout layout :on-change on-change}]]]) (mf/defc project-context-menu* diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index b3318fc6f8..2d6a98f519 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -14,6 +14,7 @@ [app.main.data.project :as dpj] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.mcp-menu :refer [dashboard-mcp-menu*]] [app.main.ui.dashboard.grid :refer [grid*]] [app.main.ui.dashboard.import :as udi] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] @@ -102,6 +103,7 @@ (:name project)]])) [:div {:class (stl/css :dashboard-header-actions)} + [:> dashboard-mcp-menu*] [:> layout-toggle* {:layout layout :on-change on-change}] diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index fef26f5b9b..2efbbefd21 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -17,6 +17,7 @@ [app.main.data.team :as dtm] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.components.mcp-menu :refer [dashboard-mcp-menu*]] [app.main.ui.dashboard.deleted :as deleted] [app.main.ui.dashboard.grid :refer [line-grid*]] [app.main.ui.dashboard.import :as udi] @@ -50,6 +51,7 @@ (tr "dashboard.personal-projects") (tr "dashboard.projects-title"))]] [:div {:class (stl/css :dashboard-header-actions)} + [:> dashboard-mcp-menu*] [:> layout-toggle* {:layout layout :on-change on-change}] (when can-edit diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 8787c290ca..2f37187244 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -23,7 +23,6 @@ [app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]] [app.main.ui.components.link :refer [link*]] - [app.main.ui.components.mcp-menu :refer [mcp-menu*]] [app.main.ui.dashboard.check-updates :as dcu] [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] @@ -256,7 +255,6 @@ (get default-project :id) team-id (get team :id) - mcp-state (mf/deref refs/mcp) projects? (= section :dashboard-recent) fonts? (= section :dashboard-fonts) @@ -384,15 +382,6 @@ [:span {:class (stl/css :element-title)} (tr "labels.shared-libraries")]]]]] - (when (and (contains? cf/flags :mcp) - (:enabled mcp-state) - (:token-valid mcp-state)) - [:div {:class (stl/css :sidebar-content-section)} - [:> mcp-menu* {:dashboard true - :is-mcp-connected (= "connected" (:connection-status mcp-state)) - :is-connection-requested (:connection-requested mcp-state) - :session-id (:session-id mcp-state)}]]) - [:div {:class (stl/css :sidebar-content-section) :data-testid "pinned-projects"} [:div {:class (stl/css :sidebar-section-title)} From cf1fbb4d7cf0f2ea3620516da5ba3f9378bcbc7f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 7 Oct 2026 13:23:47 +0200 Subject: [PATCH 067/145] :bug: Close plugins whose load is cancelled before they run Unloading or replacing a plugin while its code was still being fetched could leave a late instance running, or block reopening a plugin that closed during startup. Each load now carries an AbortController. Unloading, a newer load of the same plugin or logout aborts it, and the plugin manager closes itself on abort, also while it fetches its code. createPlugin returns nothing for a plugin closed before its sandbox exists, and failed loads clear their pending state so the plugin can be loaded again. --- plugins/CHANGELOG.md | 1 + .../src/lib/create-plugin.spec.ts | 1 + .../plugins-runtime/src/lib/create-plugin.ts | 12 +- .../src/lib/load-plugin-context.spec.ts | 3 +- .../src/lib/load-plugin-real-path.spec.ts | 189 +++++++++++++++++- .../src/lib/load-plugin.spec.ts | 13 +- .../plugins-runtime/src/lib/load-plugin.ts | 27 ++- .../plugins-runtime/src/lib/plugin-manager.ts | 21 +- 8 files changed, 241 insertions(+), 26 deletions(-) diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 94cd3f268c..433f43e8c2 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -9,6 +9,7 @@ ### 🩹 Fixes +- **plugin-runtime:** Cancel plugin loads before they execute after unloading or logout, and allow reopening plugins that close during startup. - **plugins-runtime**: An interaction obtained from `Shape.interactions` now keeps addressing that interaction instead of the position it held when the array was read. Removing every interaction of a shape from a single read removes all of them rather than leaving some behind, and writing through a held interaction after an earlier one is removed no longer lands on a different interaction. - **plugins-runtime**: `Shape.removeInteraction()` now rejects an interaction belonging to a different shape with a validation error, instead of removing whichever interaction sat at the same position on the target shape. - **plugins-runtime**: Writing `trigger`, `delay` or `action` on an interaction the shape no longer has now raises a validation error. The write used to be sent to the workspace with no position to apply it at, where it failed out of the plugin's reach: nothing was written and nothing was reported. diff --git a/plugins/libs/plugins-runtime/src/lib/create-plugin.spec.ts b/plugins/libs/plugins-runtime/src/lib/create-plugin.spec.ts index 29d0b6d691..7960f77ec2 100644 --- a/plugins/libs/plugins-runtime/src/lib/create-plugin.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/create-plugin.spec.ts @@ -83,6 +83,7 @@ describe('createPlugin', () => { manifest, expect.any(Function), expect.any(Function), + undefined, ); expect(createSandbox).toHaveBeenCalledWith(mockPluginManager, undefined); expect(mockSandbox.evaluate).toHaveBeenCalled(); diff --git a/plugins/libs/plugins-runtime/src/lib/create-plugin.ts b/plugins/libs/plugins-runtime/src/lib/create-plugin.ts index fc4ca233f3..c2b4a2f548 100644 --- a/plugins/libs/plugins-runtime/src/lib/create-plugin.ts +++ b/plugins/libs/plugins-runtime/src/lib/create-plugin.ts @@ -8,8 +8,13 @@ export async function createPlugin( manifest: Manifest, onCloseCallback: () => void, apiExtensions?: object, + signal?: AbortSignal, ) { + // Closing can happen while the manager is still fetching code. + let sandbox: ReturnType | undefined = undefined; + const evaluateSandbox = async () => { + if (plugin.destroyed || !sandbox) return; try { sandbox.evaluate(); } catch (error) { @@ -23,15 +28,18 @@ export async function createPlugin( context, manifest, function onClose() { - sandbox.cleanGlobalThis(); + sandbox?.cleanGlobalThis(); onCloseCallback(); }, function onReloadModal() { evaluateSandbox(); }, + signal, ); - const sandbox = createSandbox(plugin, apiExtensions); + if (plugin.destroyed) return; + + sandbox = createSandbox(plugin, apiExtensions); await evaluateSandbox(); diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin-context.spec.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin-context.spec.ts index 52a0267db0..9305878b3f 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin-context.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin-context.spec.ts @@ -30,7 +30,7 @@ describe('loadPlugin host context boundary (regression for #11001)', () => { close: vi.fn(), sendMessage: vi.fn(), }, - } as unknown as Awaited>); + } as unknown as NonNullable>>); }); it('does not freeze host-owned functions reachable through the context', async () => { @@ -60,6 +60,7 @@ describe('loadPlugin host context boundary (regression for #11001)', () => { manifest, expect.any(Function), undefined, + expect.any(AbortSignal), ); // Host-owned functions must remain extensible: page navigation and diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts index 2200f434b1..f921455fbe 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin-real-path.spec.ts @@ -52,15 +52,23 @@ function makeManifest( function makeHostFixture() { const listenerTypes: string[] = []; - const listeners = new Map(); + const listeners = new Map< + symbol, + { type: string; callback: (...args: unknown[]) => unknown } + >(); + const shapes: object[] = []; // Inline code (empty host + non-URL code) resolves without network, so no // fetch mock is needed. UI/modal APIs are never touched by the probe code. - const createRectangle = vi.fn(() => ({ type: 'rectangle-marker' })); + const createRectangle = vi.fn(() => { + const shape = { type: 'rectangle-marker' }; + shapes.push(shape); + return shape; + }); const selection: object[] = [{ id: 'shape-1' }]; const context = { - addListener: (type: string, _callback: (...args: unknown[]) => unknown) => { + addListener: (type: string, callback: (...args: unknown[]) => unknown) => { const id = Symbol(type); - listeners.set(id, type); + listeners.set(id, { type, callback }); listenerTypes.push(type); return id; }, @@ -68,13 +76,21 @@ function makeHostFixture() { listeners.delete(id); }, theme: 'dark', + management: { workspace: { status: 'ready' } }, createRectangle, selection, // Host-only member: present on the raw context but NOT part of the // public penpot API. Plugin code must never see it (see B-2 below). __internalSecret: 'host-internal', } as unknown as Context; - return { context, listeners, listenerTypes, createRectangle, selection }; + return { + context, + listeners, + listenerTypes, + createRectangle, + selection, + shapes, + }; } function lastCompartmentGlobalThis(): Record { @@ -182,7 +198,7 @@ describe('loadPlugin real initialization path (regression for #11001)', () => { const fixture = makeHostFixture(); setContextBuilder(() => fixture.context); const manifest = { - ...makeManifest('plugin.js', []), + ...makeManifest('plugin.js', ['content:read', 'content:write']), host: 'https://plugins.test/', scope: 'global' as const, }; @@ -207,9 +223,10 @@ describe('loadPlugin real initialization path (regression for #11001)', () => { if (order === 'after') await replacement(); finishFetch({ ok: true, - text: async () => 'globalThis.marker = "cancelled";', + text: async () => 'penpot.createRectangle();', }); await cancelled; + expect(fixture.shapes).toEqual([]); if (order === 'before') { expect(getPlugins()).toHaveLength(0); expect(fixture.listeners.size).toBe(0); @@ -225,6 +242,74 @@ describe('loadPlugin real initialization path (regression for #11001)', () => { }, ); + it('cancels a workspace load when another load replaces the same plugin', async () => { + const fixture = makeHostFixture(); + setContextBuilder(() => fixture.context); + const manifest = { + ...makeManifest('plugin.js', ['content:read', 'content:write']), + host: 'https://plugins.test/', + }; + let finishFetch!: (response: object) => void; + vi.stubGlobal( + 'fetch', + () => + new Promise((resolve) => { + finishFetch = resolve; + }), + ); + try { + const cancelled = loadPlugin(manifest); + await loadPlugin({ + ...manifest, + host: '', + code: 'globalThis.marker = "replacement";', + }); + finishFetch({ ok: true, text: async () => 'penpot.createRectangle();' }); + await cancelled; + expect(fixture.shapes).toEqual([]); + expect(getPlugins()).toHaveLength(1); + expect(lastCompartmentGlobalThis()['marker']).toBe('replacement'); + } finally { + for (const plugin of [...getPlugins()]) plugin.plugin.close(); + vi.unstubAllGlobals(); + } + }); + + it('ignores a fetch failure after a load has been cancelled', async () => { + const fixture = makeHostFixture(); + setContextBuilder(() => fixture.context); + const manifest = { + ...makeManifest('plugin.js', []), + host: 'https://plugins.test/', + scope: 'global' as const, + }; + let failFetch!: (error: Error) => void; + vi.stubGlobal( + 'fetch', + () => + new Promise((_resolve, reject) => { + failFetch = reject; + }), + ); + try { + const cancelled = loadPlugin(manifest); + ɵunloadPlugin(manifest.pluginId); + await loadPlugin({ + ...manifest, + host: '', + code: 'globalThis.marker = "replacement";', + }); + failFetch(new Error('Cancelled fetch failed')); + await cancelled; + expect(getPlugins()).toHaveLength(1); + expect(lastCompartmentGlobalThis()['marker']).toBe('replacement'); + expect(fixture.listeners.size).toBe(3); + } finally { + ɵunloadPlugin(manifest.pluginId); + vi.unstubAllGlobals(); + } + }); + it('allows retrying a global plugin after a failed load', async () => { const fixture = makeHostFixture(); setContextBuilder(() => fixture.context); @@ -315,4 +400,94 @@ describe('loadPlugin real initialization path (regression for #11001)', () => { expect(globals['penpotMgmt']).toBeUndefined(); expect(listeners.size).toBe(0); }); + + it('cancels a global load at logout before it can execute in another session', async () => { + const fixture = makeHostFixture(); + setContextBuilder(() => fixture.context); + const manifest = { + ...makeManifest('plugin.js', ['content:read', 'content:write']), + pluginId: 'logout-plugin', + host: 'https://plugins.test/', + scope: 'global' as const, + }; + let finishFetch!: (response: object) => void; + vi.stubGlobal( + 'fetch', + () => + new Promise((resolve) => { + finishFetch = resolve; + }), + ); + try { + const cancelled = loadPlugin(manifest); + for (const listener of [...fixture.listeners.values()]) { + if (listener.type === 'logout') listener.callback(); + } + await loadPlugin({ + ...manifest, + host: '', + code: 'globalThis.marker = "new-session";', + }); + finishFetch({ ok: true, text: async () => 'penpot.createRectangle();' }); + await cancelled; + expect(fixture.shapes).toEqual([]); + expect(getPlugins()).toHaveLength(1); + expect(lastCompartmentGlobalThis()['marker']).toBe('new-session'); + expect(fixture.listeners.size).toBe(3); + } finally { + ɵunloadPlugin(manifest.pluginId); + vi.unstubAllGlobals(); + } + }); + + it('cleans up a failed fetch and allows loading the plugin again', async () => { + const fixture = makeHostFixture(); + setContextBuilder(() => fixture.context); + const manifest = { + ...makeManifest('plugin.js', []), + pluginId: 'failed-fetch-plugin', + host: 'https://plugins.test/', + scope: 'global' as const, + }; + vi.stubGlobal('fetch', () => Promise.reject(new Error('Fetch failed'))); + try { + await expect(loadPlugin(manifest)).rejects.toThrow('Fetch failed'); + expect(getPlugins()).toHaveLength(0); + expect(fixture.listeners.size).toBe(0); + await loadPlugin({ + ...manifest, + host: '', + code: 'globalThis.marker = "retry";', + }); + expect(lastCompartmentGlobalThis()['marker']).toBe('retry'); + } finally { + ɵunloadPlugin(manifest.pluginId); + vi.unstubAllGlobals(); + } + }); + + it('does not register a global plugin that closes during startup and permits reopening', async () => { + const fixture = makeHostFixture(); + setContextBuilder(() => fixture.context); + const manifest = { + ...makeManifest('penpot.closePlugin();', [ + 'content:read', + 'content:write', + ]), + pluginId: 'self-closing-plugin', + scope: 'global' as const, + }; + try { + await loadPlugin(manifest); + expect(getPlugins()).toHaveLength(0); + expect(fixture.listeners.size).toBe(0); + await loadPlugin({ ...manifest, code: 'penpot.createRectangle();' }); + expect(fixture.shapes).toEqual([{ type: 'rectangle-marker' }]); + expect(getPlugins()).toHaveLength(1); + } finally { + ɵunloadPlugin(manifest.pluginId); + } + expect(getPlugins()).toHaveLength(0); + expect(fixture.listeners.size).toBe(0); + }); }); diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts index 779619279c..eeffb3d236 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts @@ -33,7 +33,7 @@ vi.mock('./ses.js', () => ({ describe('plugin-loader', () => { let mockContext: Context; let manifest: Manifest; - let mockPluginApi: Awaited>; + let mockPluginApi: NonNullable>>; let mockClose: ReturnType; beforeEach(() => { @@ -61,7 +61,7 @@ describe('plugin-loader', () => { close: mockClose, sendMessage: vi.fn(), }, - } as unknown as Awaited>; + } as unknown as NonNullable>>; mockContext = { addListener: vi.fn(), @@ -84,6 +84,7 @@ describe('plugin-loader', () => { manifest, expect.any(Function), undefined, + expect.any(AbortSignal), ); expect(mockPluginApi.plugin.close).not.toHaveBeenCalled(); expect(getPlugins()).toHaveLength(1); @@ -129,7 +130,7 @@ describe('plugin-loader', () => { }, iframeWindow: mockIframeWindow, manifest: { ...manifest, host: 'http://localhost:4202' }, - } as unknown as Awaited>; + } as unknown as NonNullable>>; vi.mocked(createPlugin).mockResolvedValue(mockPluginWithIframe); @@ -173,7 +174,7 @@ describe('plugin-loader', () => { }, iframeWindow: mockIframeWindow1, manifest: { ...manifest, host: 'http://localhost:4202' }, - } as unknown as Awaited>; + } as unknown as NonNullable>>; const mockPluginApi2 = { plugin: { @@ -182,7 +183,7 @@ describe('plugin-loader', () => { }, iframeWindow: mockIframeWindow2, manifest: { ...manifest, host: 'http://localhost:4203' }, - } as unknown as Awaited>; + } as unknown as NonNullable>>; vi.mocked(createPlugin).mockResolvedValue(mockPluginApi1); await loadPlugin(manifest); @@ -250,6 +251,7 @@ describe('plugin-loader', () => { manifest, expect.any(Function), undefined, + expect.any(AbortSignal), ); }); @@ -265,6 +267,7 @@ describe('plugin-loader', () => { manifest, expect.any(Function), undefined, + expect.any(AbortSignal), ); }); }); diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts index bad0d4fd63..8da32c4d64 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.ts @@ -4,8 +4,10 @@ import { loadManifest } from './parse-manifest.js'; import { Manifest } from './models/manifest.model.js'; import { createPlugin } from './create-plugin.js'; -let plugins: Awaited>[] = []; -const pendingPlugins = new Map(); +type Plugin = NonNullable>>; + +let plugins: Plugin[] = []; +const pendingPlugins = new Map(); export type ContextBuilder = (id: string) => Context; @@ -50,7 +52,7 @@ export const loadPlugin = async function ( closeCallback?: () => void, apiExtensions?: object, ) { - const loadId = Symbol(); + const load = new AbortController(); try { const context = contextBuilder && contextBuilder(manifest.pluginId); @@ -86,13 +88,17 @@ export const loadPlugin = async function ( // `createSandbox`'s proxy handler applies `ses.safeReturn` to values // crossing into the sandbox. Compartment isolation and intrinsics // hardening are performed by createSandbox, not here. - pendingPlugins.set(manifest.pluginId, loadId); - let plugin: Awaited> | undefined = - undefined; + pendingPlugins.get(manifest.pluginId)?.abort(); + pendingPlugins.set(manifest.pluginId, load); + let plugin: Plugin | undefined = undefined; plugin = await createPlugin( context, manifest, () => { + load.abort(); + if (pendingPlugins.get(manifest.pluginId) === load) { + pendingPlugins.delete(manifest.pluginId); + } plugins = plugins.filter((api) => api !== plugin); if (closeCallback) { @@ -100,9 +106,10 @@ export const loadPlugin = async function ( } }, apiExtensions, + load.signal, ); - if (pendingPlugins.get(manifest.pluginId) !== loadId) { - plugin.plugin.close(); + if (!plugin || load.signal.aborted) { + plugin?.plugin.close(); return; } plugins.push(plugin); @@ -110,7 +117,7 @@ export const loadPlugin = async function ( if (manifest.scope !== 'global') closeAllPlugins(); throw error; } finally { - if (pendingPlugins.get(manifest.pluginId) === loadId) { + if (pendingPlugins.get(manifest.pluginId) === load) { pendingPlugins.delete(manifest.pluginId); } } @@ -130,7 +137,7 @@ export const ɵloadPluginByUrl = async function (manifestUrl: string) { }; export const ɵunloadPlugin = function (id: Manifest['pluginId']) { - pendingPlugins.delete(id); + pendingPlugins.get(id)?.abort(); const plugin = plugins.find((plugin) => plugin.manifest.pluginId === id); if (plugin) { diff --git a/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts b/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts index f430321210..3cb6a9c261 100644 --- a/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts +++ b/plugins/libs/plugins-runtime/src/lib/plugin-manager.ts @@ -14,8 +14,9 @@ export async function createPluginManager( manifest: Manifest, onCloseCallback: () => void, onReloadModal: (code: string) => void, + signal?: AbortSignal, ) { - let code = await loadManifestCode(manifest); + let code = ''; let loaded = false; let destroyed = false; @@ -61,6 +62,7 @@ export async function createPluginManager( const closePlugin = () => { if (destroyed) return; destroyed = true; + signal?.removeEventListener('abort', closePlugin); removeAllEventListeners(); destroyListener(listenerId); destroyListener(logoutId); @@ -148,7 +150,24 @@ export async function createPluginManager( context.removeListener(listenerId); }; + signal?.addEventListener('abort', closePlugin, { once: true }); + try { + if (signal?.aborted) { + closePlugin(); + } else { + code = await loadManifestCode(manifest); + } + } catch (error) { + if (!destroyed) { + closePlugin(); + throw error; + } + } + return { + get destroyed() { + return destroyed; + }, close: closePlugin, destroyListener, openModal, From a381761aa4e2fd82f8aa75a0a9b475126e6125e2 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 7 Oct 2026 13:24:05 +0200 Subject: [PATCH 068/145] :sparkles: Add project and file creation to penpotMgmt Add penpotMgmt.createProject({name, teamId?}) and penpotMgmt.createFile({projectId, name}). Both require content:write, trim the name and require 1 to 250 characters, and return the same metadata as listProjects and listFiles without changing the current view. When the new project or file belongs to the current team, the dashboard state is updated so it shows up right away. listProjects, listFiles and the creation methods share the summary builders, so fileCount is now 0 instead of missing for new projects. Update the plugin types, the MCP API docs and the MCP instructions. --- frontend/src/app/plugins/management.cljs | 104 ++++++++++++--- .../plugins/management_test.cljs | 126 ++++++++++++++++++ mcp/packages/server/data/api_types.yml | 59 +++++++- .../server/data/initial_instructions.md | 6 +- plugins/CHANGELOG.md | 1 + plugins/libs/plugin-types/index.d.ts | 46 ++++++- .../libs/plugins-runtime/src/lib/api/index.ts | 8 ++ .../src/lib/api/plugin-api.spec.ts | 88 ++++++++++++ 8 files changed, 412 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/plugins/management.cljs b/frontend/src/app/plugins/management.cljs index 9dadac543b..0337da56ba 100644 --- a/frontend/src/app/plugins/management.cljs +++ b/frontend/src/app/plugins/management.cljs @@ -6,13 +6,34 @@ (ns app.plugins.management (:require + [app.common.features :as cfeat] [app.common.uuid :as uuid] + [app.main.data.dashboard :as dd] [app.main.data.helpers :as dsh] [app.main.repo :as rp] [app.main.router :as rt] [app.main.store :as st] [app.util.object :as obj] - [beicon.v2.core :as rx])) + [beicon.v2.core :as rx] + [clojure.set :as set] + [cuerdas.core :as str] + [potok.v2.core :as ptk])) + +(defn- project-summary + [project] + #js {:id (str (:id project)) + :teamId (str (:team-id project)) + :name (:name project) + :isDefault (boolean (:is-default project)) + :fileCount (or (:count project) 0)}) + +(defn- file-summary + [file] + #js {:id (str (:id file)) + :teamId (str (:team-id file)) + :projectId (str (:project-id file)) + :name (:name file) + :modifiedAt (.toISOString ^js (:modified-at file))}) (defn workspace-context [state] @@ -83,14 +104,7 @@ (fn [resolve reject] (->> (rp/cmd! :get-projects {:team-id team-id}) (rx/map (fn [projects] - (clj->js - (mapv (fn [project] - {:id (str (:id project)) - :teamId (str (:team-id project)) - :name (:name project) - :isDefault (boolean (:is-default project)) - :fileCount (:count project)}) - (remove :deleted-at projects))))) + (into-array (map project-summary (remove :deleted-at projects))))) (rx/subs! resolve reject))))))) (defn list-files @@ -102,16 +116,68 @@ (fn [resolve reject] (->> (rp/cmd! :get-project-files {:project-id project-id}) (rx/map (fn [files] - (clj->js - (mapv (fn [file] - {:id (str (:id file)) - :teamId (str (:team-id file)) - :projectId (str (:project-id file)) - :name (:name file) - :modifiedAt (.toISOString ^js (:modified-at file))}) - files)))) + (into-array (map file-summary files)))) (rx/subs! resolve reject))))))) +(defn- creation-name + [options] + (let [name (obj/get options "name")] + (when (string? name) + (let [name (str/trim name)] + (when (<= 1 (count name) 250) + name))))) + +(defn create-project + [options] + (let [team-id (if-let [id (obj/get options "teamId")] + (uuid/parse* id) + (:current-team-id @st/state)) + name (creation-name options)] + (cond + (nil? team-id) + (js/Promise.reject (js/Error. "Expected a team UUID")) + + (nil? name) + (js/Promise.reject (js/Error. "Expected a name with 1 to 250 characters")) + + :else + (js/Promise. + (fn [resolve reject] + (->> (rp/cmd! :create-project {:team-id team-id :name name}) + (rx/tap (fn [project] + (st/emit! + (fn [state] + (if (= (:team-id project) (:current-team-id state)) + (assoc-in state [:projects (:id project)] (assoc project :count 0)) + state))))) + (rx/map project-summary) + (rx/subs! resolve reject))))))) + +(defn create-file + [options] + (let [project-id (uuid/parse* (obj/get options "projectId")) + name (creation-name options)] + (cond + (nil? project-id) + (js/Promise.reject (js/Error. "Expected a project UUID")) + + (nil? name) + (js/Promise.reject (js/Error. "Expected a name with 1 to 250 characters")) + + :else + (js/Promise. + (fn [resolve reject] + (let [features (set/difference (:features @st/state #{}) cfeat/frontend-only-features)] + (->> (rp/cmd! :create-file {:project-id project-id :name name :features features}) + (rx/tap (fn [file] + (st/emit! + (fn [state] + (if (= (:team-id file) (:current-team-id state)) + (ptk/update (dd/file-created file) state) + state))))) + (rx/map file-summary) + (rx/subs! resolve reject)))))))) + (defn create-context [] (obj/reify {:name "PenpotManagementContext"} @@ -119,4 +185,6 @@ {:get (fn [] (clj->js (workspace-context @st/state)))} :openFile open-file :listProjects list-projects - :listFiles list-files)) + :listFiles list-files + :createProject create-project + :createFile create-file)) diff --git a/frontend/test/frontend_tests/plugins/management_test.cljs b/frontend/test/frontend_tests/plugins/management_test.cljs index 9db0ae80c0..eb74e0b09d 100644 --- a/frontend/test/frontend_tests/plugins/management_test.cljs +++ b/frontend/test/frontend_tests/plugins/management_test.cljs @@ -152,3 +152,129 @@ (mock/with-mocks* {rp/cmd! (mock/stub (fn [_ _] (rx/throw error)))} (t/is (identical? error (await (.then (list-items) (fn [] nil) identity))))))))) + +(t/deftest ^:async create-project-returns-metadata-and-updates-only-the-current-team + (let [project-id (uuid/next) + other-team-id (uuid/next)] + (doseq [[initial options target-team-id] + [[dashboard #js {:name " Buttons "} team-id] + [ready #js {:name " Buttons " :teamId (str other-team-id)} other-team-id]]] + (let [state (atom initial) + requests (atom []) + project {:id project-id :team-id target-team-id :name "Buttons" + :is-default false :is-pinned false}] + (await + (mock/with-mocks* + {st/state state + st/emit! (mock/stub (fn [event] (swap! state event))) + rp/cmd! (mock/stub + (fn [command params] + (swap! requests conj [command params]) + (->> (rx/of project) (rx/observe-on :async))))} + (let [context (management/create-context) + result (await (.createProject context options))] + (t/is (= {"id" (str project-id) "teamId" (str target-team-id) + "name" "Buttons" "isDefault" false "fileCount" 0} + (js->clj result))) + (t/is (= [[:create-project {:team-id target-team-id :name "Buttons"}]] @requests)) + (t/is (= (if (= team-id target-team-id) + (assoc-in initial [:projects project-id] (assoc project :count 0)) + initial) + @state))))))))) + +(t/deftest ^:async create-file-returns-metadata-and-updates-the-dashboard-without-navigation + (let [project-id (uuid/next) + created-id (uuid/next) + modified-at (js/Date. "2026-01-02T03:04:05Z")] + (doseq [initial [dashboard ready]] + (let [initial (assoc initial + :features #{"components/v2" "styles/v2" "plugins/runtime"} + :projects {project-id {:id project-id :team-id team-id :count 2}}) + state (atom initial) + requests (atom []) + file {:id created-id :team-id team-id :project-id project-id + :name "Login" :modified-at modified-at :data {:pages []}}] + (await + (mock/with-mocks* + {st/state state + st/emit! (mock/stub (fn [event] (swap! state event))) + rp/cmd! (mock/stub + (fn [command params] + (swap! requests conj [command params]) + (->> (rx/of file) (rx/observe-on :async))))} + (let [context (management/create-context) + result (await (.createFile context #js {:projectId (str project-id) :name " Login " + :id "ignored" :isShared true}))] + (t/is (= {"id" (str created-id) "teamId" (str team-id) + "projectId" (str project-id) "name" "Login" + "modifiedAt" "2026-01-02T03:04:05.000Z"} + (js->clj result))) + (t/is (= [[:create-file {:project-id project-id :name "Login" :features #{"components/v2"}}]] + @requests)) + (t/is (= (-> initial + (assoc-in [:files created-id] (dissoc file :data)) + (assoc-in [:recent-files created-id] (dissoc file :data)) + (update-in [:projects project-id :count] inc)) + @state))))))))) + +(t/deftest ^:async creation-rejects-invalid-input-before-querying + (let [project-id (uuid/next) + requests (atom [])] + (await + (mock/with-mocks* + {st/state (atom dashboard) + rp/cmd! (mock/stub (fn [command params] + (swap! requests conj [command params]) + (->> (rx/of nil) (rx/observe-on :async))))} + (let [context (management/create-context)] + (doseq [options [nil #js {} #js {:name nil} #js {:name 1} + #js {:name ""} #js {:name " \n "} + #js {:name (apply str (repeat 251 "a"))}]] + (t/is (= "Expected a name with 1 to 250 characters" + (await (.then (.createProject context options) (fn [] nil) #(.-message %))))) + (let [options (js/Object.assign #js {:projectId (str project-id)} options)] + (t/is (= "Expected a name with 1 to 250 characters" + (await (.then (.createFile context options) (fn [] nil) #(.-message %))))))) + (doseq [[create options message] + [[#(.createProject context %) #js {:name "Buttons" :teamId "invalid"} "Expected a team UUID"] + [#(.createFile context %) #js {:name "Login"} "Expected a project UUID"] + [#(.createFile context %) #js {:name "Login" :projectId "invalid"} "Expected a project UUID"]]] + (t/is (= message (await (.then (create options) (fn [] nil) #(.-message %))))))) + (await (a/settle)) + (t/is (empty? @requests)))))) + +(t/deftest ^:async creation-preserves-backend-errors-and-state + (doseq [method ["createProject" "createFile"]] + (let [state (atom dashboard) + error (js/Error. "Access denied")] + (await + (mock/with-mocks* + {st/state state + st/emit! (mock/stub (fn [event] (swap! state event))) + rp/cmd! (mock/stub (fn [_ _] (->> (rx/throw error) (rx/observe-on :async))))} + (let [context (management/create-context) + options #js {:name "Design" :projectId (str (uuid/next))} + result (.call (aget context method) context options)] + (t/is (identical? error (await (.then result (fn [] nil) identity)))) + (t/is (= dashboard @state)))))))) + +(t/deftest ^:async creation-does-not-add-results-to-a-different-team-after-navigation + (doseq [method ["createProject" "createFile"]] + (let [state (atom dashboard) + response (rx/subject) + other (assoc dashboard :current-team-id (uuid/next)) + item-id (uuid/next) + item {:id item-id :team-id team-id :project-id (uuid/next) + :name "Design" :modified-at (js/Date.)}] + (await + (mock/with-mocks* + {st/state state + st/emit! (mock/stub (fn [event] (swap! state event))) + rp/cmd! (mock/stub (fn [_ _] (->> response (rx/observe-on :async))))} + (let [context (management/create-context) + result (.call (aget context method) context + #js {:name "Design" :projectId (str (:project-id item))})] + (reset! state other) + (rx/push! response item) + (t/is (= (str item-id) (.-id (await result)))) + (t/is (= other @state)))))))) diff --git a/mcp/packages/server/data/api_types.yml b/mcp/packages/server/data/api_types.yml index c926147938..1fbccd549f 100644 --- a/mcp/packages/server/data/api_types.yml +++ b/mcp/packages/server/data/api_types.yml @@ -5128,9 +5128,9 @@ PenpotMgmt: Application management API exposed as `penpotMgmt` to global plugins. - * Requires `content:read` permission. + * Discovery and navigation require `content:read`; creation requires `content:write`. * Available in the dashboard and file workspaces. - * Use this API to discover and open files; file operations on `penpot` require a ready workspace. + * Use this API to discover, create and open files; file operations on `penpot` require a ready workspace. * Reacquire file, page and shape objects after changing files. ``` @@ -5138,6 +5138,8 @@ PenpotMgmt: workspace: WorkspaceContext; listProjects(options?: { teamId?: string }): Promise; listFiles(options: { projectId: string }): Promise; + createProject(options: { name: string; teamId?: string }): Promise; + createFile(options: { name: string; projectId: string }): Promise; openFile(fileId: string, options?: { teamId?: string }): Promise; on( type: "workspacechange", @@ -5199,6 +5201,51 @@ PenpotMgmt: ``` const files = await penpotMgmt.listFiles({ projectId: project.id }); ``` + createProject: |- + ``` + createProject(options: { name: string; teamId?: string }): Promise + ``` + + Creates a project and returns its metadata snapshot. Requires `content:write`. + + * `teamId` defaults to the current team. + * Trims `name` and requires 1 to 250 characters. + * Does not change navigation or open a file. + * Rejects invalid input, backend access errors and quota errors. + + Parameters + + * options: { name: string; teamId?: string } + + Returns Promise + + Example + ``` + const project = await penpotMgmt.createProject({ name: 'Design system' }); + ``` + createFile: |- + ``` + createFile(options: { name: string; projectId: string }): Promise + ``` + + Creates a file with an initial page and returns its metadata snapshot. Requires `content:write`. + + * Requires an explicit `projectId`. + * Trims `name` and requires 1 to 250 characters. + * Does not open the file or change navigation. Use `openFile` to edit it. + * Rejects invalid input, backend access errors and quota errors. + + Parameters + + * options: { name: string; projectId: string } + + Returns Promise + + Example + ``` + const file = await penpotMgmt.createFile({ projectId: project.id, name: 'Login' }); + await penpotMgmt.openFile(file.id, { teamId: file.teamId }); + ``` openFile: |- ``` openFile(fileId: string, options?: { teamId?: string }): Promise @@ -5260,7 +5307,9 @@ Context: "workspace" | "openFile" | "listProjects" - | "listFiles", + | "listFiles" + | "createProject" + | "createFile", >; version: string; root: Shape | null; @@ -5340,7 +5389,9 @@ Context: "workspace" | "openFile" | "listProjects" - | "listFiles", + | "listFiles" + | "createProject" + | "createFile", > ``` version: |- diff --git a/mcp/packages/server/data/initial_instructions.md b/mcp/packages/server/data/initial_instructions.md index ea5961c605..6fd27c2e81 100644 --- a/mcp/packages/server/data/initial_instructions.md +++ b/mcp/packages/server/data/initial_instructions.md @@ -435,7 +435,11 @@ For many tasks, it can be critical to visually inspect the design. Remember to u # The `penpotMgmt` Object This object (of type `PenpotMgmt`) serves to manage Penpot design projects and files. -It allows you to discover and navigate to different design files the user has access to. +It allows you to discover, create and navigate to design projects and files the user has access to. +Create a project with `await penpotMgmt.createProject({ name: "Design system" })` (optionally specify `teamId`). +Create a file with `await penpotMgmt.createFile({ projectId: project.id, name: "Login" })`. +Both creation methods require `content:write` and return metadata without changing the current view. +To edit the new file, call `await penpotMgmt.openFile(file.id, { teamId: file.teamId })` and reacquire file, page and shape objects. You navigate to different design files only when your task explicitly requires it/you are instructed to do so. -- diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 433f43e8c2..ab324f891c 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -2,6 +2,7 @@ ### 🚀 Features +- **plugin-types, plugins-runtime:** Added `penpotMgmt.createProject` and `penpotMgmt.createFile` to create projects and files from the dashboard or workspace with `content:write` permission. - **plugins-runtime, plugin-types**: Added global plugins (`scope: "global"`) that remain active across dashboard and workspace navigation until closed or logged out, with `penpotMgmt` for project and file discovery, file navigation and workspace events. - **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently. - **plugin-types**: Added `waitForLayoutUpdate` to wait until pending layout updates have finished. It rejects when the optional timeout elapses, defaulting to 30 seconds so a wait never hangs. diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index d1d145fe96..f375f42cd3 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -809,9 +809,9 @@ export interface FileSummary { /** * Application management API exposed as `penpotMgmt` to global plugins. * - * - Requires `content:read` permission. + * - Discovery and navigation require `content:read`; creation requires `content:write`. * - Available in the dashboard and file workspaces. - * - Use this API to discover and open files; file operations on `penpot` require a ready workspace. + * - Use this API to discover, create and open files; file operations on `penpot` require a ready workspace. * - Reacquire file, page and shape objects after changing files. */ export interface PenpotMgmt { @@ -843,6 +843,41 @@ export interface PenpotMgmt { * ``` */ listFiles(options: { projectId: string }): Promise; + /** + * Creates a project and returns its metadata snapshot. Requires `content:write`. + * + * - `teamId` defaults to the current team. + * - Trims `name` and requires 1 to 250 characters. + * - Does not change navigation or open a file. + * - Rejects invalid input, backend access errors and quota errors. + * + * @example + * ```ts + * const project = await penpotMgmt.createProject({ name: 'Design system' }); + * ``` + */ + createProject(options: { + name: string; + teamId?: string; + }): Promise; + /** + * Creates a file with an initial page and returns its metadata snapshot. Requires `content:write`. + * + * - Requires an explicit `projectId`. + * - Trims `name` and requires 1 to 250 characters. + * - Does not open the file or change navigation. Use `openFile` to edit it. + * - Rejects invalid input, backend access errors and quota errors. + * + * @example + * ```ts + * const file = await penpotMgmt.createFile({ projectId: project.id, name: 'Login' }); + * await penpotMgmt.openFile(file.id, { teamId: file.teamId }); + * ``` + */ + createFile(options: { + name: string; + projectId: string; + }): Promise; /** * Opens a file in this tab and resolves once its workspace and page are ready. * @@ -869,7 +904,12 @@ export interface PenpotMgmt { export interface Context { readonly management?: Pick< PenpotMgmt, - 'workspace' | 'openFile' | 'listProjects' | 'listFiles' + | 'workspace' + | 'openFile' + | 'listProjects' + | 'listFiles' + | 'createProject' + | 'createFile' >; /** * Returns the current penpot version. diff --git a/plugins/libs/plugins-runtime/src/lib/api/index.ts b/plugins/libs/plugins-runtime/src/lib/api/index.ts index ba36b1e12a..44f5645cc5 100644 --- a/plugins/libs/plugins-runtime/src/lib/api/index.ts +++ b/plugins/libs/plugins-runtime/src/lib/api/index.ts @@ -427,6 +427,14 @@ export function createApi( checkPermission('content:read', false); return plugin.context.management!.listFiles(options); }, + createProject(options) { + checkPermission('content:write', false); + return plugin.context.management!.createProject(options); + }, + createFile(options) { + checkPermission('content:write', false); + return plugin.context.management!.createFile(options); + }, openFile(fileId, options) { checkPermission('content:read', false); return plugin.context.management!.openFile(fileId, options); diff --git a/plugins/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts b/plugins/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts index 3513b97acd..e53eddacc3 100644 --- a/plugins/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/api/plugin-api.spec.ts @@ -108,6 +108,94 @@ describe('Plugin api', () => { }); }); + describe('management creation', () => { + it.each(['none', 'loading', 'ready'])( + 'creates projects and files with workspace status %s', + async (status) => { + const project = { id: 'project-id', name: 'Buttons' }; + const file = { id: 'file-id', name: 'Login' }; + const management = { + workspace: { status }, + createProject: vi.fn().mockResolvedValue(project), + createFile: vi.fn().mockResolvedValue(file), + }; + const { penpotMgmt } = createApi({ + ...pluginManager, + manifest: { + ...pluginManager.manifest, + permissions: ['content:write'], + scope: 'global', + }, + context: { ...pluginManager.context, management }, + } as any); + + await expect( + penpotMgmt.createProject({ name: 'Buttons' }), + ).resolves.toEqual(project); + await expect( + penpotMgmt.createProject({ name: 'Buttons', teamId: 'team-id' }), + ).resolves.toEqual(project); + expect(management.createProject).toHaveBeenLastCalledWith({ + name: 'Buttons', + teamId: 'team-id', + }); + await expect( + penpotMgmt.createFile({ name: 'Login', projectId: 'project-id' }), + ).resolves.toEqual(file); + expect(management.createFile).toHaveBeenCalledWith({ + name: 'Login', + projectId: 'project-id', + }); + }, + ); + + it.each([{ permissions: [] }, { permissions: ['content:read'] }])( + 'requires content:write before creating with permissions $permissions', + ({ permissions }) => { + const management = { + createProject: vi.fn(), + createFile: vi.fn(), + }; + const { penpotMgmt } = createApi({ + ...pluginManager, + manifest: { ...pluginManager.manifest, permissions, scope: 'global' }, + context: { ...pluginManager.context, management }, + } as any); + + expect(() => penpotMgmt.createProject({ name: 'Buttons' })).toThrow( + 'Permission content:write is not granted', + ); + expect(() => + penpotMgmt.createFile({ name: 'Login', projectId: 'project-id' }), + ).toThrow('Permission content:write is not granted'); + expect(management.createProject).not.toHaveBeenCalled(); + expect(management.createFile).not.toHaveBeenCalled(); + }, + ); + + it('preserves backend creation errors', async () => { + const error = new Error('Access denied'); + const { penpotMgmt } = createApi({ + ...pluginManager, + manifest: { ...pluginManager.manifest, scope: 'global' }, + context: { + ...pluginManager.context, + management: { + createProject: vi.fn().mockRejectedValue(error), + createFile: vi.fn().mockRejectedValue(error), + }, + }, + } as any); + + await expect(penpotMgmt.createProject({ name: 'Buttons' })).rejects.toBe( + error, + ); + await expect( + penpotMgmt.createFile({ name: 'Login', projectId: 'project-id' }), + ).rejects.toBe(error); + }); + }); + describe('ui', () => { describe.concurrent('permissions', () => { const api = createApi({ From dfff188409799865d6b05151d43e8c55af813c31 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 7 Oct 2026 13:24:12 +0200 Subject: [PATCH 069/145] :sparkles: Ask users to allow global plugins on install A plugin with "scope": "global" keeps running across the dashboard and files and can find and open the user's projects and files. Any manifest could ask for this without the user being told. Add the allow:global permission. The plugin registry adds it to every manifest with global scope, so the install dialog lists it, and a plugin that becomes global in a later version shows the permissions update dialog again. The permission is valid in the shared schema, which the backend checks when saving, and in the runtime manifest schema. The built-in MCP plugin declares it too. Document the scope property and the new permission for plugin developers. --- .../test/backend_tests/rpc_plugins_test.clj | 11 ++++++++++ common/src/app/common/types/plugins.cljc | 5 +++-- docs/plugins/getting-started.md | 4 ++++ frontend/src/app/main/data/workspace/mcp.cljs | 3 ++- .../src/app/main/ui/workspace/plugins.cljs | 7 +++++++ frontend/src/app/plugins/register.cljs | 17 +++++++++------ .../frontend_tests/plugins/register_test.cljs | 21 +++++++++++++++++++ frontend/translations/en.po | 6 ++++++ frontend/translations/es.po | 6 ++++++ plugins/CHANGELOG.md | 2 +- .../src/lib/models/manifest.schema.ts | 1 + 11 files changed, 73 insertions(+), 10 deletions(-) diff --git a/backend/test/backend_tests/rpc_plugins_test.clj b/backend/test/backend_tests/rpc_plugins_test.clj index 8c57931844..8d6be84dc6 100644 --- a/backend/test/backend_tests/rpc_plugins_test.clj +++ b/backend/test/backend_tests/rpc_plugins_test.clj @@ -47,6 +47,17 @@ (t/is (= [plugin-id-1] (:ids plugins))) (t/is (= valid-plugin (get-in plugins [:data plugin-id-1])))))) +(t/deftest add-profile-plugin-accepts-global-plugins + (let [profile (th/create-profile* 1) + plugin (assoc valid-plugin + :scope "global" + :permissions #{"content:read" "allow:global"}) + out (th/command! {::th/type :add-profile-plugin + ::rpc/profile-id (:id profile) + :plugin plugin})] + (t/is (nil? (:error out))) + (t/is (= plugin (:result out))))) + (t/deftest add-profile-plugin-rejects-invalid-permissions (let [profile (th/create-profile* 1) plugin (assoc valid-plugin :permissions #{"content:read" "admin:delete"}) diff --git a/common/src/app/common/types/plugins.cljc b/common/src/app/common/types/plugins.cljc index b9a1ca5330..8db72497fd 100644 --- a/common/src/app/common/types/plugins.cljc +++ b/common/src/app/common/types/plugins.cljc @@ -35,11 +35,12 @@ "clipboard:read" "clipboard:write" "user:read" "allow:downloads" - "allow:localstorage"}) + "allow:localstorage" + "allow:global"}) (def schema:permissions "Schema for plugin permissions - a set of valid permission strings." - [:set {:gen/max 11} (into [:enum] (sort valid-permissions))]) + [:set {:gen/max 12} (into [:enum] (sort valid-permissions))]) (def max-plugins "Maximum number of plugins a profile can hold." diff --git a/docs/plugins/getting-started.md b/docs/plugins/getting-started.md index ea993640a9..5f72ac4ada 100644 --- a/docs/plugins/getting-started.md +++ b/docs/plugins/getting-started.md @@ -160,6 +160,7 @@ Set "version": 2 in your - **Code**: your plugin's file location. It needs to be compiled to JavaScript and reachable. - **Icon**: your plugin's icon, which will be also displayed in the plugin manager modal. It'll be a tag so you can use whichever image format works better for you. **It's recommended to use a 56x56 pixel icon for the best appearance on all devices**. - **Permissions**: your plugin's permissions, which allow access to different parts of the Penpot API. +- **Scope** (optional): "workspace" (the default) or "global". A workspace plugin closes when its file closes. A global plugin keeps running while the user moves between the dashboard and files, and gets the penpotMgmt object to find, create and open projects and files. Global scope adds the allow:global permission, which the user must accept. #### Types of permissions @@ -185,6 +186,9 @@ Typical use cases: downloading the full project file for backup or sharing. - allow:localstorage: Allows the access to the local storage proxy to store information. This info is only available for the plugin installation but be aware that a user can see this information in the browser. Typical use cases: storing authentication tokens for a plugin login +- allow:global: Allows the plugin to keep running while the user moves between the dashboard and files, and to find and open their projects and files. Added automatically when the manifest sets "scope": "global". +Typical use cases: assistants or integrations that work across several files. + _Note: Write permissions automatically includes its corresponding read permission (e.g., content:write includes content:read) because reading is required to perform write or modification actions._ ### What are plugin.ts and plugin.js files? diff --git a/frontend/src/app/main/data/workspace/mcp.cljs b/frontend/src/app/main/data/workspace/mcp.cljs index f3e3817239..e4b03fb956 100644 --- a/frontend/src/app/main/data/workspace/mcp.cljs +++ b/frontend/src/app/main/data/workspace/mcp.cljs @@ -42,7 +42,8 @@ #{"user:read" "library:read" "library:write" "comment:read" "comment:write" - "content:write" "content:read"}}) + "content:write" "content:read" + "allow:global"}}) (defonce interval-sub (atom nil)) diff --git a/frontend/src/app/main/ui/workspace/plugins.cljs b/frontend/src/app/main/ui/workspace/plugins.cljs index a6ff1b34f0..12bd45e766 100644 --- a/frontend/src/app/main/ui/workspace/plugins.cljs +++ b/frontend/src/app/main/ui/workspace/plugins.cljs @@ -331,6 +331,13 @@ [:p {:class (stl/css :permissions-list-text)} (tr "workspace.plugins.permissions.allow-localstorage")]]) + (cond + (contains? permissions "allow:global") + [:div {:class (stl/css :permissions-list-entry)} + deprecated-icon/oauth-1 + [:p {:class (stl/css :permissions-list-text)} + (tr "workspace.plugins.permissions.allow-global")]]) + (cond (contains? permissions "clipboard:write") [:div {:class (stl/css :permissions-list-entry)} diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs index fc709427d4..8b5bcc3bde 100644 --- a/frontend/src/app/plugins/register.cljs +++ b/frontend/src/app/plugins/register.cljs @@ -54,15 +54,20 @@ "Read the manifest.json defined by the plugins definition and transforms it into an object that will be stored in the register." [plugin-url ^js manifest] - (let [name (obj/get manifest "name") - desc (obj/get manifest "description") - code (obj/get manifest "code") - icon (obj/get manifest "icon") - vers (d/nilv (obj/get manifest "version") 1) + (let [name (obj/get manifest "name") + desc (obj/get manifest "description") + code (obj/get manifest "code") + icon (obj/get manifest "icon") + vers (d/nilv (obj/get manifest "version") 1) + scope (obj/get manifest "scope") permissions (into #{} (obj/get manifest "permissions" [])) permissions (cond-> permissions + ;; Global plugins must be explicitly allowed by the user + (= "global" scope) + (conj "allow:global") + (contains? permissions "content:write") (conj "content:read") @@ -102,7 +107,7 @@ {:plugin-id plugin-id :url (str plugin-url) :version vers - :scope (obj/get manifest "scope") + :scope scope :name name :description desc :host origin diff --git a/frontend/test/frontend_tests/plugins/register_test.cljs b/frontend/test/frontend_tests/plugins/register_test.cljs index 55ddd1c4a6..49c387d4bc 100644 --- a/frontend/test/frontend_tests/plugins/register_test.cljs +++ b/frontend/test/frontend_tests/plugins/register_test.cljs @@ -320,3 +320,24 @@ (t/is (= [true false] @seen) "no calls after unsubscribing") (done')) done)))) + +;; --- parse-manifest --- + +(t/deftest parse-manifest-adds-allow-global-for-global-scope + (let [manifest (preg/parse-manifest + "http://localhost:4400/manifest.json" + #js {:name "Global plugin" + :code "plugin.js" + :scope "global" + :permissions #js ["content:read"]})] + (t/is (= "global" (:scope manifest))) + (t/is (= #{"content:read" "allow:global"} (:permissions manifest))))) + +(t/deftest parse-manifest-keeps-workspace-plugins-without-allow-global + (let [manifest (preg/parse-manifest + "http://localhost:4400/manifest.json" + #js {:name "Workspace plugin" + :code "plugin.js" + :permissions #js ["content:read"]})] + (t/is (nil? (:scope manifest))) + (t/is (= #{"content:read"} (:permissions manifest))))) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index d71621ab02..18000c059e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -8180,6 +8180,12 @@ msgstr "" msgid "workspace.plugins.permissions.allow-download" msgstr "Start file downloads." +#: src/app/main/ui/workspace/plugins.cljs:339 +msgid "workspace.plugins.permissions.allow-global" +msgstr "" +"Keep running while you move between the dashboard and your files, and find " +"and open your projects and files." + #: src/app/main/ui/workspace/plugins.cljs:332 msgid "workspace.plugins.permissions.allow-localstorage" msgstr "Store data in the browser." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index c1464fe980..03bac196ed 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -8276,6 +8276,12 @@ msgstr "" msgid "workspace.plugins.permissions.allow-download" msgstr "Comenzar descargas de ficheros." +#: src/app/main/ui/workspace/plugins.cljs:339 +msgid "workspace.plugins.permissions.allow-global" +msgstr "" +"Seguir funcionando mientras te mueves entre el panel y tus archivos, y " +"buscar y abrir tus proyectos y archivos." + #: src/app/main/ui/workspace/plugins.cljs:332 msgid "workspace.plugins.permissions.allow-localstorage" msgstr "Guardar datos en el navegador." diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index ab324f891c..041d252969 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -3,7 +3,7 @@ ### 🚀 Features - **plugin-types, plugins-runtime:** Added `penpotMgmt.createProject` and `penpotMgmt.createFile` to create projects and files from the dashboard or workspace with `content:write` permission. -- **plugins-runtime, plugin-types**: Added global plugins (`scope: "global"`) that remain active across dashboard and workspace navigation until closed or logged out, with `penpotMgmt` for project and file discovery, file navigation and workspace events. +- **plugins-runtime, plugin-types**: Added global plugins (`scope: "global"`) that remain active across dashboard and workspace navigation until closed or logged out, with `penpotMgmt` for project and file discovery, file navigation and workspace events. Global scope adds the `allow:global` permission, which users must accept when installing the plugin. - **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently. - **plugin-types**: Added `waitForLayoutUpdate` to wait until pending layout updates have finished. It rejects when the optional timeout elapses, defaulting to 30 seconds so a wait never hangs. - **plugin-types**: Added `waitForLayoutUpdate` to the `Shape` interface to wait until the pending layout updates of a shape and its children have finished diff --git a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts index aefc563c65..357fe9f95f 100644 --- a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts +++ b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts @@ -20,6 +20,7 @@ export const manifestSchema = z.object({ 'comment:write', 'allow:downloads', 'allow:localstorage', + 'allow:global', 'clipboard:read', 'clipboard:write', ]), From a125977fce0d4786224050a19a1b62adabfde627 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 7 Oct 2026 13:24:17 +0200 Subject: [PATCH 070/145] :bug: Keep the MCP plugin working without penpotMgmt Penpot only exposes penpotMgmt to plugins running with global scope. Without it, the MCP plugin failed on load when registering its workspacechange listener. When penpotMgmt is missing, the plugin now builds the workspace metadata from the open file and skips the listener. execute_code receives penpotMgmt as null and waits for layout updates as before. The tool description and instructions tell agents that only the open file is available when penpotMgmt is null. --- .../plugin/src/ConnectionMetadata.test.ts | 38 +++++++++++++++++++ mcp/packages/plugin/src/index.d.ts | 3 +- mcp/packages/plugin/src/plugin.ts | 29 ++++++++++++-- .../task-handlers/ExecuteCodeTaskHandler.ts | 5 ++- .../server/data/initial_instructions.md | 2 + .../server/src/tools/ExecuteCodeTool.ts | 1 + 6 files changed, 72 insertions(+), 6 deletions(-) diff --git a/mcp/packages/plugin/src/ConnectionMetadata.test.ts b/mcp/packages/plugin/src/ConnectionMetadata.test.ts index 91aadb8188..3131086153 100644 --- a/mcp/packages/plugin/src/ConnectionMetadata.test.ts +++ b/mcp/packages/plugin/src/ConnectionMetadata.test.ts @@ -192,6 +192,44 @@ test("plugin reads the current file for each connection metadata request", () => ]); }); +test("plugin without penpotMgmt derives metadata from the open file", () => { + let onMessage!: (message: any) => void; + const messages: any[] = []; + const penpot = { + currentFile: { id: "file-1", name: "Design" } as { id: string; name: string } | null, + currentUser: { sessionId: "tab-1" }, + theme: "dark", + ui: { + open() {}, + onMessage: (handler: (message: any) => void) => { + onMessage = handler; + }, + sendMessage: (message: any) => messages.push(JSON.parse(JSON.stringify(message))), + }, + on() {}, + }; + runInNewContext(compile("plugin.ts"), { + exports: {}, + penpot, + mcp: undefined, + require: () => ({ ExecuteCodeTaskHandler: class {} }), + }); + onMessage({ type: "connection-metadata-request", sessionId: "first" }); + penpot.currentFile = null; + onMessage({ type: "context-request" }); + assert.deepEqual(messages, [ + { + type: "initialize", + penpotUserSessionId: "tab-1", + session: { sessionId: "first", fileId: "file-1", fileName: "Design", workspaceState: "ready" }, + }, + { + type: "context-update", + context: { fileId: null, fileName: null, workspaceState: "none" }, + }, + ]); +}); + test("integrated connections reuse the short session ID across reconnects", async () => { const ui = pluginUi(); ui.message({ type: "mcp-mode", integratedRemoteMcp: true }); diff --git a/mcp/packages/plugin/src/index.d.ts b/mcp/packages/plugin/src/index.d.ts index 67a498275f..b53afdef68 100644 --- a/mcp/packages/plugin/src/index.d.ts +++ b/mcp/packages/plugin/src/index.d.ts @@ -8,7 +8,8 @@ interface McpOptions { declare global { const mcp: undefined | McpOptions; - const penpotMgmt: import("../../../../plugins/libs/plugin-types").PenpotMgmt; + /** management API; only exposed to plugins running with global scope */ + const penpotMgmt: undefined | null | import("../../../../plugins/libs/plugin-types").PenpotMgmt; } export {}; diff --git a/mcp/packages/plugin/src/plugin.ts b/mcp/packages/plugin/src/plugin.ts index bb55706a10..b3e8c691bc 100644 --- a/mcp/packages/plugin/src/plugin.ts +++ b/mcp/packages/plugin/src/plugin.ts @@ -9,6 +9,12 @@ import type { PluginConnectionInit } from "../../common/src"; */ const isIntegratedRemoteMcp = !!mcp; +/** + * the management API, which Penpot only exposes to plugins running with global scope; + * null otherwise (e.g. in Penpot versions without global plugins) + */ +const management = typeof penpotMgmt === "undefined" ? null : penpotMgmt; + /** * Extracts the major.minor.patch prefix from a version string. * @@ -66,7 +72,7 @@ penpot.ui.onMessage< }); } } else if (typeof message === "object" && message.type === "connection-metadata-request") { - const workspace = penpotMgmt.workspace; + const workspace = currentWorkspace(); if (message.sessionId) { const initialization: PluginConnectionInit & { penpotUserSessionId?: string } = { type: "initialize", @@ -92,9 +98,26 @@ penpot.ui.onMessage< } }); +/** + * Provides the workspace metadata of this tab. + * Without the management API, the metadata is derived from the open file. + */ +function currentWorkspace(): NonNullable["workspace"] { + if (management) { + return management.workspace; + } + const file = penpot.currentFile; + return { + status: file ? "ready" : "none", + fileId: file?.id ?? null, + fileName: file?.name ?? null, + teamId: null, + }; +} + /** Sends the current workspace metadata without replacing the connection. */ function sendWorkspaceContext(): void { - const workspace = penpotMgmt.workspace; + const workspace = currentWorkspace(); penpot.ui.sendMessage({ type: "context-update", context: { @@ -105,7 +128,7 @@ function sendWorkspaceContext(): void { }); } -penpotMgmt.on("workspacechange", sendWorkspaceContext); +management?.on("workspacechange", sendWorkspaceContext); /** * Handles plugin task requests received from the MCP server via WebSocket. diff --git a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts index 2132446f80..47004e5cfe 100644 --- a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts +++ b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts @@ -178,7 +178,7 @@ export class ExecuteCodeTaskHandler extends TaskHandler { // initialize context, making penpot, penpotUtils, storage and the custom console available this.context = { penpot: penpot, - penpotMgmt: penpotMgmt, + penpotMgmt: typeof penpotMgmt === "undefined" ? null : penpotMgmt, storage: {}, console: new ExecuteCodeTaskConsole(), penpotUtils: PenpotUtils, @@ -217,8 +217,9 @@ export class ExecuteCodeTaskHandler extends TaskHandler { try { // wait for layout updates prior to executing the supplied code (if method is available) try { + const management = this.context.penpotMgmt; // @ts-ignore - TODO Penpot.waitForLayoutUpdate is not yet in the released types - if (penpotMgmt.workspace.status === "ready" && penpot.waitForLayoutUpdate) { + if ((!management || management.workspace.status === "ready") && penpot.waitForLayoutUpdate) { // @ts-ignore await penpot.waitForLayoutUpdate(); } diff --git a/mcp/packages/server/data/initial_instructions.md b/mcp/packages/server/data/initial_instructions.md index 6fd27c2e81..437c58c7ab 100644 --- a/mcp/packages/server/data/initial_instructions.md +++ b/mcp/packages/server/data/initial_instructions.md @@ -441,6 +441,8 @@ Create a file with `await penpotMgmt.createFile({ projectId: project.id, name: " Both creation methods require `content:write` and return metadata without changing the current view. To edit the new file, call `await penpotMgmt.openFile(file.id, { teamId: file.teamId })` and reacquire file, page and shape objects. You navigate to different design files only when your task explicitly requires it/you are instructed to do so. +If `penpotMgmt` is `null`, file management is unavailable and only the file that is open in Penpot can be used; +ask the user to open another file if needed. -- You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again. diff --git a/mcp/packages/server/src/tools/ExecuteCodeTool.ts b/mcp/packages/server/src/tools/ExecuteCodeTool.ts index 7a8e710e33..98932a154a 100644 --- a/mcp/packages/server/src/tools/ExecuteCodeTool.ts +++ b/mcp/packages/server/src/tools/ExecuteCodeTool.ts @@ -50,6 +50,7 @@ export class ExecuteCodeTool extends Tool { "IMPORTANT: Before using this tool, make sure you have read the 'Penpot High-Level Overview' and know " + "which Penpot API functionality is necessary and how to use it.\n" + "You have access to `penpot` (of type `Penpot`), `penpotMgmt` (of type `PenpotMgmt`), `penpotUtils`, and `storage`.\n" + + "If `penpotMgmt` is null, file management is unavailable and only the open file can be used.\n" + "`storage` is an object in which arbitrary data can be stored, simply by adding a new attribute; " + "stored attributes can be referenced in future calls to this tool, so any intermediate results that " + "could come in handy later should be stored in `storage` instead of just a fleeting variable; " + From 9517f2ac9426f8058808f7df690acb23ad24d8f1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 7 Oct 2026 15:20:32 +0200 Subject: [PATCH 071/145] :recycle: Type the MCP plugin with the repo plugin types Link the MCP plugin to plugins/libs/plugin-types instead of the published 1.5.0 package, which lacks PenpotMgmt. The repo types declare the penpotMgmt global, so the inline import in index.d.ts goes away and the plugin checks against the API it runs with. Type Context.management as PenpotMgmt without on/off, so new management methods need no extra listing. Mark the background plugin mock in load-plugin.spec.ts as NonNullable: createPlugin can return undefined, and the build type-check failed on it. --- mcp/packages/plugin/package.json | 2 +- mcp/packages/plugin/src/index.d.ts | 2 -- mcp/packages/plugin/src/plugin.ts | 3 ++- mcp/packages/server/data/api_types.yml | 23 ++++--------------- mcp/pnpm-lock.yaml | 9 ++------ plugins/libs/plugin-types/index.d.ts | 14 ++++------- .../src/lib/load-plugin.spec.ts | 2 +- 7 files changed, 16 insertions(+), 39 deletions(-) diff --git a/mcp/packages/plugin/package.json b/mcp/packages/plugin/package.json index 1a44059515..d4a3723755 100644 --- a/mcp/packages/plugin/package.json +++ b/mcp/packages/plugin/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@penpot/plugin-styles": "1.5.0", - "@penpot/plugin-types": "1.5.0" + "@penpot/plugin-types": "link:../../../plugins/libs/plugin-types" }, "devDependencies": { "cross-env": "^10.1.0", diff --git a/mcp/packages/plugin/src/index.d.ts b/mcp/packages/plugin/src/index.d.ts index b53afdef68..1fa11567bb 100644 --- a/mcp/packages/plugin/src/index.d.ts +++ b/mcp/packages/plugin/src/index.d.ts @@ -8,8 +8,6 @@ interface McpOptions { declare global { const mcp: undefined | McpOptions; - /** management API; only exposed to plugins running with global scope */ - const penpotMgmt: undefined | null | import("../../../../plugins/libs/plugin-types").PenpotMgmt; } export {}; diff --git a/mcp/packages/plugin/src/plugin.ts b/mcp/packages/plugin/src/plugin.ts index b3e8c691bc..025354df74 100644 --- a/mcp/packages/plugin/src/plugin.ts +++ b/mcp/packages/plugin/src/plugin.ts @@ -2,6 +2,7 @@ import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler"; import { Task, TaskHandler } from "./TaskHandler"; import { formatTaskError } from "./ErrorUtils"; import type { PluginConnectionInit } from "../../common/src"; +import type { WorkspaceContext } from "@penpot/plugin-types"; /** * indicates whether the plugin is running in an environment with the Penpot-integrated remote MCP server @@ -102,7 +103,7 @@ penpot.ui.onMessage< * Provides the workspace metadata of this tab. * Without the management API, the metadata is derived from the open file. */ -function currentWorkspace(): NonNullable["workspace"] { +function currentWorkspace(): WorkspaceContext { if (management) { return management.workspace; } diff --git a/mcp/packages/server/data/api_types.yml b/mcp/packages/server/data/api_types.yml index 1fbccd549f..7fc050cec7 100644 --- a/mcp/packages/server/data/api_types.yml +++ b/mcp/packages/server/data/api_types.yml @@ -5302,15 +5302,7 @@ Context: ``` interface Context { - management?: Pick< - PenpotMgmt, - "workspace" - | "openFile" - | "listProjects" - | "listFiles" - | "createProject" - | "createFile", - >; + management?: Omit; version: string; root: Shape | null; currentFile: File | null; @@ -5384,16 +5376,11 @@ Context: Properties: management: |- ``` - readonly management?: Pick< - PenpotMgmt, - "workspace" - | "openFile" - | "listProjects" - | "listFiles" - | "createProject" - | "createFile", - > + readonly management?: Omit ``` + + Backs `penpotMgmt`. Its events go through the plugin listener registry, + so `on` and `off` are not part of it. version: |- ``` readonly version: string diff --git a/mcp/pnpm-lock.yaml b/mcp/pnpm-lock.yaml index 1ebfccbe6d..854a012147 100644 --- a/mcp/pnpm-lock.yaml +++ b/mcp/pnpm-lock.yaml @@ -188,8 +188,8 @@ importers: specifier: 1.5.0 version: 1.5.0 '@penpot/plugin-types': - specifier: 1.5.0 - version: 1.5.0 + specifier: link:../../../plugins/libs/plugin-types + version: link:../../../plugins/libs/plugin-types devDependencies: cross-env: specifier: ^10.1.0 @@ -663,9 +663,6 @@ packages: '@penpot/plugin-styles@1.5.0': resolution: {integrity: sha512-rwXFBRPd0IZs3ens+eRT8oA5WqkQgF6xlW6fCfH7U1NHRjej9HH7vTGXmfjoIVml6Xx1sbyYHZVxkQC8Ub8+zA==} - '@penpot/plugin-types@1.5.0': - resolution: {integrity: sha512-hFk4Rx66rLaSUWoN6bhxXkDI2ymwKg9xokdslvZvXM5MSmEmfhsJKUMwtuzgYWljbbfaQhK+DjK8g8yVbbavtA==} - '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -1880,8 +1877,6 @@ snapshots: '@penpot/plugin-styles@1.5.0': {} - '@penpot/plugin-types@1.5.0': {} - '@pinojs/redact@0.4.0': {} '@rolldown/binding-android-arm-eabi@1.2.11': diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index f375f42cd3..031f7cc3ba 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -902,15 +902,11 @@ export interface PenpotMgmt { * Represents the context of Penpot, providing access to various Penpot functionalities and data. */ export interface Context { - readonly management?: Pick< - PenpotMgmt, - | 'workspace' - | 'openFile' - | 'listProjects' - | 'listFiles' - | 'createProject' - | 'createFile' - >; + /** + * Backs `penpotMgmt`. Its events go through the plugin listener registry, + * so `on` and `off` are not part of it. + */ + readonly management?: Omit; /** * Returns the current penpot version. */ diff --git a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts index eeffb3d236..853c1d12c7 100644 --- a/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts +++ b/plugins/libs/plugins-runtime/src/lib/load-plugin.spec.ts @@ -216,7 +216,7 @@ describe('plugin-loader', () => { pluginId: 'background-plugin', allowBackground: true, }, - } as unknown as Awaited>; + } as unknown as NonNullable>>; vi.mocked(createPlugin).mockResolvedValue(backgroundPluginApi); await loadPlugin(manifest); From dcf0b197eb387279b56d629aa00a87b4c3b69956 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 7 Oct 2026 15:45:10 +0200 Subject: [PATCH 072/145] :bug: Fix inner shadow covering the shape in the SVG renderer (#12141) * :bug: Fix inner shadow covering the shape in the SVG renderer The svg filter components moved to the `*` syntax (#9448), but filter-entry* still built their props as a raw JS object with kebab-case keys. Raw JS props skip the kebab to camel conversion, so the components read nil for filterId and filterIn, and no filter primitive got its result or in2. With the chain broken, an inner shadow was blended over the empty flood instead of the shape, hiding the fill (and a board's children) under the shadow color. Exports using these filters broke the same way. Pass the props in camelCase. Closes #11967 AI-assisted-by: claude-opus-5-5 * :recycle: Build filter entry props with mf/props Use mf/props instead of a raw JS object so the props keep the kebab-case keys used across the codebase. The macro converts them to camelCase at compile time, so there is no runtime cost, and the comment about the manual conversion is no longer needed. AI-assisted-by: claude-opus-5-5 --- frontend/src/app/main/ui/shapes/filters.cljs | 6 +-- frontend/test/frontend_tests/runner.cljs | 2 + .../frontend_tests/ui/shape_filters_test.cljs | 47 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 frontend/test/frontend_tests/ui/shape_filters_test.cljs diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index a4468d649d..bf82f08be0 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -116,9 +116,9 @@ :result filter-id}]) (mf/defc filter-entry* [{:keys [entry]}] - (let [props #js {:filter-id (:id entry) - :filter-in (:filter-in entry) - :params (:params entry)}] + (let [props (mf/props {:filter-id (:id entry) + :filter-in (:filter-in entry) + :params (:params entry)})] (case (:type entry) :drop-shadow [:> drop-shadow-filter* props] :inner-shadow [:> inner-shadow-filter* props] diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index a17d9b3149..daaae968a0 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -124,6 +124,7 @@ [frontend-tests.ui.routes-test] [frontend-tests.ui.settings-password-schema-test] [frontend-tests.ui.settings-shortcuts-test] + [frontend-tests.ui.shape-filters-test] [frontend-tests.ui.shortcuts-labels-test] [frontend-tests.ui.sidebar-scroll-test] [frontend-tests.ui.stroke-menu-test] @@ -273,6 +274,7 @@ 'frontend-tests.text-editor-paste-guard-test 'frontend-tests.ui.settings-password-schema-test 'frontend-tests.ui.settings-shortcuts-test + 'frontend-tests.ui.shape-filters-test 'frontend-tests.ui.shortcuts-labels-test 'frontend-tests.ui.sidebar-scroll-test 'frontend-tests.ui.stroke-menu-test diff --git a/frontend/test/frontend_tests/ui/shape_filters_test.cljs b/frontend/test/frontend_tests/ui/shape_filters_test.cljs new file mode 100644 index 0000000000..0cfc20b149 --- /dev/null +++ b/frontend/test/frontend_tests/ui/shape_filters_test.cljs @@ -0,0 +1,47 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.ui.shape-filters-test + (:require + ["react-dom/server" :as rds] + [app.common.types.shape :as cts] + [app.common.uuid :as uuid] + [app.main.ui.shapes.filters :as filters] + [cljs.test :as t :include-macros true] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn- render-filter + [shadow-style] + (let [shape (-> (cts/setup-shape {:type :rect :x 0 :y 0 :width 100 :height 100}) + (assoc :fills [{:fill-color "#4488ff" :fill-opacity 1}] + :shadow [{:id (uuid/next) + :style shadow-style + :offset-x 4 :offset-y 4 :blur 4 :spread 0 + :hidden false + :color {:color "#000000" :opacity 0.5}}]))] + (rds/renderToStaticMarkup + (mf/element filters/filters* #js {:filterId "filter" :shape shape})))) + +(defn- tags + [markup tag] + (re-seq (re-pattern (str "<" tag "[^>]*>")) markup)) + +;; Each filter primitive must name its result and read the previous one, or +;; the chain breaks: an inner shadow then covers the shape fill with the +;; shadow color (#11967). + +(t/deftest inner-shadow-filter-chain-is-wired + (let [markup (render-filter :inner-shadow) + blends (tags markup "feBlend")] + (t/is (str/includes? (first (tags markup "feFlood")) "result=")) + (t/is (= 2 (count blends))) + (t/is (every? #(str/includes? % "in2=") blends)) + (t/is (every? #(str/includes? % "result=") blends)))) + +(t/deftest drop-shadow-filter-chain-is-wired + (let [blends (tags (render-filter :drop-shadow) "feBlend")] + (t/is (every? #(and (str/includes? % "in2=") (str/includes? % "result=")) blends)))) From 30127e57a0139b665a6fe84f435ebe8cb7524675 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 7 Oct 2026 18:03:51 +0200 Subject: [PATCH 073/145] :bug: Skip spurious save when missing font cannot be remapped (#12164) fix-deleted-fonts committed text and typography changes even when no replacement font existed, so opening a page with a missing custom font rewrote identical content and cleared position-data on every visit. Only emit changes when the remapped content or typography actually differs. Add focused unit coverage for the no-replacement and remapped paths. Closes #12151 --- .../data/workspace/fix_deleted_fonts.cljs | 28 ++++--- .../data/fix_deleted_fonts_test.cljs | 75 +++++++++++++++++++ frontend/test/frontend_tests/runner.cljs | 2 + 3 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 frontend/test/frontend_tests/data/fix_deleted_fonts_test.cljs diff --git a/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs b/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs index 0f90250ca0..7a69602045 100644 --- a/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs +++ b/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs @@ -65,15 +65,18 @@ [{:keys [objects id]}] (reduce-kv (fn [changes shape-id shape] (if (shape-has-invalid-font-family?? shape) - (conj changes {:type :mod-obj - :id shape-id - :page-id id - :operations [{:type :set - :attr :content - :val (fix-shape-content shape)} - {:type :set - :attr :position-data - :val nil}]}) + (let [fixed-content (fix-shape-content shape)] + (if (not= fixed-content (:content shape)) + (conj changes {:type :mod-obj + :id shape-id + :page-id id + :operations [{:type :set + :attr :content + :val fixed-content} + {:type :set + :attr :position-data + :val nil}]}) + changes)) changes)) [] objects)) @@ -82,8 +85,11 @@ [fdata] (reduce-kv (fn [changes _ typography] (if (has-invalid-font-family? typography) - (conj changes {:type :mod-typography - :typography (fix-typography typography)}) + (let [fixed-typography (fix-typography typography)] + (if (not= fixed-typography typography) + (conj changes {:type :mod-typography + :typography fixed-typography}) + changes)) changes)) [] (:typographies fdata))) diff --git a/frontend/test/frontend_tests/data/fix_deleted_fonts_test.cljs b/frontend/test/frontend_tests/data/fix_deleted_fonts_test.cljs new file mode 100644 index 0000000000..4592c90ab3 --- /dev/null +++ b/frontend/test/frontend_tests/data/fix_deleted_fonts_test.cljs @@ -0,0 +1,75 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.data.fix-deleted-fonts-test + (:require + [app.common.test-helpers.files :as cthf] + [app.common.test-helpers.ids-map :as cthi] + [app.common.test-helpers.shapes :as cths] + [app.common.types.shape :as cts] + [app.common.types.text :as txt] + [app.main.data.changes :as dwc] + [app.main.data.workspace.fix-deleted-fonts :as fdf] + [app.main.fonts :as fonts] + [beicon.v2.core :as rx] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.state :as ths] + [potok.v2.core :as ptk])) + +(t/use-fixtures :each + {:before cthi/reset-idmap!}) + +(def ^:private missing-font-id + "custom-missing-font-id") + +(defn- text-content-with-font + [text font-id font-family] + (-> (cts/setup-shape {:type :text :x 0 :y 0 :grow-type :auto-width}) + :content + (txt/change-text text :font-id font-id :font-family font-family))) + +(defn- file-with-text-shape + [font-id font-family] + (-> (cthf/sample-file :file1 :page-label :page1) + (cths/add-sample-shape :text + {:type :text + :content (text-content-with-font "TEST" + font-id + font-family)}))) + +(defn- watch-fix-deleted-fonts-events + [file] + (let [events (atom []) + store (ths/setup-store file) + state @store] + (when-let [result (ptk/watch (fdf/fix-deleted-fonts-for-page (:id file) + (cthf/current-page-id file)) + state + (rx/empty))] + (->> result (rx/subs! #(swap! events conj %)))) + @events)) + +(t/deftest fix-deleted-fonts-skips-unfixable-missing-font + (let [file (file-with-text-shape missing-font-id "Gibson") + events (watch-fix-deleted-fonts-events file)] + (t/is (empty? events) + "missing custom fonts with no replacement must not commit changes"))) + +(t/deftest fix-deleted-fonts-remaps-when-replacement-exists + (let [replacement-id "custom-gibson-replacement" + cleanup #(swap! fonts/fontsdb dissoc replacement-id)] + (try + (swap! fonts/fontsdb assoc replacement-id + {:id replacement-id + :family "Gibson" + :name "Gibson" + :variants []}) + (let [file (file-with-text-shape missing-font-id "Gibson") + events (watch-fix-deleted-fonts-events file)] + (t/is (= 1 (count events))) + (t/is (ptk/type? ::dwc/commit-changes (first events)))) + (finally + (cleanup))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index adb408ceb4..493dddeb42 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -12,6 +12,7 @@ [frontend-tests.data.dashboard-test] [frontend-tests.data.event-test] [frontend-tests.data.exports-assets-test] + [frontend-tests.data.fix-deleted-fonts-test] [frontend-tests.data.nitrate-test] [frontend-tests.data.persistence-retry-test] [frontend-tests.data.persistence-test] @@ -150,6 +151,7 @@ 'frontend-tests.data.comments-filters-test 'frontend-tests.data.dashboard-test 'frontend-tests.data.event-test + 'frontend-tests.data.fix-deleted-fonts-test 'frontend-tests.data.nitrate-test 'frontend-tests.data.persistence-retry-test 'frontend-tests.data.persistence-test From f252439d843a20449421e5e2d8ca15a97eb12678 Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Wed, 7 Oct 2026 18:19:34 +0200 Subject: [PATCH 074/145] :recycle: Factor GL context initialization out of canvas init (#11931) * :recycle: Factor GL context initialization out of canvas init - fixes setting of gl-context-handle to be after _init so an exception does not leave invalid state behind. - api.init-canvas-context uses a new webgl.init-context! - this decouples config from init, so we can reuse it in benchmarks - some helpers move to the webgl ns AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3 * :bug: Release acquired GL context when renderer init fails - Properly release resources if init fails - Rethrown exceptions with the original cause - Fix vacuous check in clear-canvas AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3 * :sparkles: Test webgl context init and release AI-assisted-by: muse-spark, glm-5.3 * :lipstick: Address review comments * :lipstick: Drop `!` from some function names --- .../common/render_wasm/serialize_shape.cljs | 6 +- exporter/src/app/wasm/serialize.cljs | 4 +- frontend/src/app/render_wasm/api.cljs | 146 ++++++------------ frontend/src/app/render_wasm/api/webgl.cljs | 136 +++++++++++++++- .../render_wasm/serialization_test.cljs | 10 +- .../render_wasm/webgl_test.cljs | 105 +++++++++++++ frontend/test/frontend_tests/runner.cljs | 2 + 7 files changed, 300 insertions(+), 109 deletions(-) create mode 100644 frontend/test/frontend_tests/render_wasm/webgl_test.cljs diff --git a/common/src/app/common/render_wasm/serialize_shape.cljs b/common/src/app/common/render_wasm/serialize_shape.cljs index 1df4a84505..b97fc8599a 100644 --- a/common/src/app/common/render_wasm/serialize_shape.cljs +++ b/common/src/app/common/render_wasm/serialize_shape.cljs @@ -48,7 +48,7 @@ (when (and (contains? #{:path :bool} type) (some? (:content shape))) (props/set-shape-path-content (:content shape))))) -(defn serialize-shape! +(defn serialize-shape "Applies every host-independent WASM property of `shape`." [shape] (let [shape (svg-derived/apply-svg-derived shape)] @@ -59,11 +59,11 @@ shape)) -(defn serialize-shapes-batch! +(defn serialize-shapes-batch "Structural batch upload plus per-shape svg-attrs/path tail. Derives via svg-derived, uploads one `_set_shapes_batch` with `opts`, - then selects each shape needing svg-attrs/path content via `use-shape!` + then selects each shape needing svg-attrs/path content via `use-shape` and applies the tail. Host text/grid/image sequencing stays in callers. Returns the prepared vector for the downstream host-attrs loop." [shapes opts] diff --git a/exporter/src/app/wasm/serialize.cljs b/exporter/src/app/wasm/serialize.cljs index 6f316187c7..93071eaa31 100644 --- a/exporter/src/app/wasm/serialize.cljs +++ b/exporter/src/app/wasm/serialize.cljs @@ -23,12 +23,12 @@ (defn set-shape! "Serializes a single shape into the WASM design state. The host-independent properties (base props, children, blur, shadows, svg-attrs, mask, bool-type, - path geometry, grow-type) go through the shared `serialize-shape!` — the same + path geometry, grow-type) go through the shared `serialize-shape` — the same code the workspace's `set-object` uses, so the two can't drift. Only the host-specific parts are handled here: fills/strokes (image bytes are provisioned separately) and text content (fonts provisioned separately)." [shape] - (let [prepared (serialize-shape/serialize-shape! shape) + (let [prepared (serialize-shape/serialize-shape shape) type (:type prepared)] (props/write-shape-fills! (:fills prepared)) (when-not (= type :group) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 59ad963352..93f840767e 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -276,8 +276,6 @@ (def text-editor-select-paragraph text-editor/text-editor-select-paragraph) (def text-editor-sync-content text-editor/text-editor-sync-content) -(def dpr - (if use-dpr? (if (exists? js/window) js/window.devicePixelRatio 1.0) 1.0)) (defn get-dpr "Returns the current device pixel ratio. Use instead of `dpr` wherever @@ -1132,7 +1130,7 @@ (h/call wasm/internal-module "_set_shape_hidden" hidden)) (defn clear-shape-fills! - "Clear the fills of the currently-selected shape (call `use-shape!` first). + "Clear the fills of the currently-selected shape (call `use-shape` first). Equivalent to `set-shape-fills` with an empty collection." [] (when (initialized?) @@ -1590,7 +1588,7 @@ `skip-fills-strokes?` is true, fill/stroke records were already in the batch; only image fetches remain. - Always `use-shape!` first: after a multi-shape batch the WASM current shape is + Always `use-shape` first: after a multi-shape batch the WASM current shape is the last record in the chunk, not this shape." [shape skip-layout? & {:keys [skip-fills-strokes?] :or {skip-fills-strokes? false}}] (let [id (dm/get-prop shape :id) @@ -1645,7 +1643,7 @@ {:thumbnails [] :full [] :font-face-keys #{} :pending-font-face-keys #{}} (do (perf/begin-measure "set-object") - (let [prepared (serialize-shape/serialize-shape! shape) + (let [prepared (serialize-shape/serialize-shape shape) result (set-object-host-attrs prepared false)] (perf/end-measure "set-object") result)))) @@ -1813,7 +1811,7 @@ end-index (min total (+ start-index BATCH_MAX_SHAPES)) chunk (into [] (subvec (if (vector? shapes) shapes (vec shapes)) start-index end-index)) - prepared (serialize-shape/serialize-shapes-batch! + prepared (serialize-shape/serialize-shapes-batch chunk {:include-layout? true :include-fills-strokes? true})] @@ -1962,7 +1960,7 @@ (defn- set-objects-sync "Synchronously process all shapes (for small shape counts)." [shapes render-callback on-shapes-ready] - (let [prepared (serialize-shape/serialize-shapes-batch! + (let [prepared (serialize-shape/serialize-shapes-batch shapes {:include-layout? true :include-fills-strokes? true}) @@ -2336,13 +2334,6 @@ (fn [resolve _reject] (timers/raf (fn [] (resolve nil)))))) -(def ^:private default-context-options - #js {:antialias false - :depth true - :stencil true - :alpha true - "preserveDrawingBuffer" true}) - (defn resize-viewbox "Resizes the WASM viewbox. No-ops unless the GL context is live (`wasm/live?`), so callers cannot hit `_resize_viewbox` during @@ -2377,54 +2368,19 @@ (when (wasm/live?) (h/call wasm/internal-module "_set_render_options" (debug-flags) new-dpr))) -(def ^:private max-surface-size - ;; Must match `gpu_state::MAX_SURFACE_SIZE`. - 8192) - -(defn- clamp-physical-size - "Clamp physical pixel dimensions before assigning `canvas.width/height`. - Rust `resize` applies the same cap and syncs the effective DPR from the - real drawing buffer." - [w h] - (let [w (mth/max 1 w) - h (mth/max 1 h) - scale (mth/min 1 (/ max-surface-size w) (/ max-surface-size h))] - [(mth/max 1 (mth/floor (* scale w))) - (mth/max 1 (mth/floor (* scale h)))])) - (defn resize-offscreen-canvas! "Resize a persistent OffscreenCanvas to new physical-pixel dimensions and update the WASM render surfaces accordingly (via `_resize_viewbox`). The design state (shape pool) is preserved so `set-objects` is not needed again." [canvas new-physical-w new-physical-h] (when (wasm/live?) - (let [dpr (get-dpr) - [pw ph] (clamp-physical-size new-physical-w new-physical-h)] + (let [dpr (get-dpr) + [pw ph] (webgl/clamp-physical-size new-physical-w new-physical-h)] (set! (.-width canvas) pw) (set! (.-height canvas) ph) (set-render-options! dpr) (resize-viewbox (/ new-physical-w dpr) (/ new-physical-h dpr))))) -(defn- wasm-set-param-from-route-params-if-present - [param-name] - (when-let [value (wasm-get-numeric-value param-name)] - (let [setter-name (str/concat "_set_" (name param-name))] - (h/call wasm/internal-module setter-name value)))) - -(defn- canvas-css-size - "Return canvas size in CSS pixels. - - - For DOM canvases: use `clientWidth/clientHeight`. - - For OffscreenCanvas: fall back to `width/height` (physical px) converted by DPR." - [canvas dpr] - (let [cw (.-clientWidth ^js canvas) - ch (.-clientHeight ^js canvas)] - (if (and (number? cw) (pos? cw) - (number? ch) (pos? ch)) - [cw ch] - [(/ (.-width ^js canvas) dpr) - (/ (.-height ^js canvas) dpr)]))) - (defn resize-canvas! "Sizes the canvas drawing buffer, the WASM render surface and the DPR from a single source of truth (the canvas CSS client size) so the GL framebuffer @@ -2436,10 +2392,10 @@ (resize-canvas! canvas (get-dpr))) ([canvas new-dpr] (when (wasm/live?) - (let [[css-w css-h] (canvas-css-size canvas new-dpr) - css-w (mth/max 1 css-w) - css-h (mth/max 1 css-h) - [phys-w phys-h] (clamp-physical-size + (let [[css-w css-h] (webgl/canvas-css-size canvas new-dpr) + css-w (mth/max 1 css-w) + css-h (mth/max 1 css-h) + [phys-w phys-h] (webgl/clamp-physical-size (mth/floor (* css-w new-dpr)) (mth/floor (* css-h new-dpr)))] (set! (.-width ^js canvas) phys-w) @@ -2503,45 +2459,40 @@ [canvas] (if-not (wasm/module-ready?) false - (let [gl (unchecked-get wasm/internal-module "GL") - flags (debug-flags) + (let [flags (debug-flags) context-id (if (dbg/enabled? :wasm-gl-context-init-error) "fail" "webgl2") - context (.getContext ^js canvas context-id default-context-options) - context-init? (not (nil? context)) - browser (sr/translate-browser cf/browser) - dpr (get-dpr) - [css-w css-h] (canvas-css-size canvas dpr) + browser (sr/translate-browser cf/browser) + dpr (get-dpr) + [css-w css-h] (webgl/canvas-css-size canvas dpr) ;; Avoid 0×0 Skia/GL surfaces (crashes on some browsers). - css-w (mth/max 1 css-w) - css-h (mth/max 1 css-h) + css-w (mth/max 1 css-w) + css-h (mth/max 1 css-h) + params {:antialias_threshold (wasm-get-numeric-value :antialias_threshold) + :viewport_interest_area_threshold (wasm-get-numeric-value :viewport_interest_area_threshold) + :max_blocking_time_ms (wasm-get-numeric-value :max_blocking_time_ms) + :node_batch_threshold (wasm-get-numeric-value :node_batch_threshold) + :blur_downscale_threshold (wasm-get-numeric-value :blur_downscale_threshold)} + result (webgl/init-context canvas {:module wasm/internal-module + :context-id context-id + :css-width css-w + :css-height css-h + :dpr dpr + :flags flags + :browser browser + :params params}) + context-init? (some? result) can-listen? (fn? (.-addEventListener ^js canvas))] - (when-not (nil? context) - (let [handle (.registerContext ^js gl context #js {"majorVersion" 2})] - (.makeContextCurrent ^js gl handle) + (when result + (let [{:keys [context handle]} result] (set! wasm/gl-context-handle handle) (set! wasm/gl-context context) - ;; Force the WEBGL_debug_renderer_info extension as emscripten does not enable it - (.getExtension context "WEBGL_debug_renderer_info") - - ;; Initialize Wasm Render Engine - (h/call wasm/internal-module "_init" css-w css-h) - (h/call wasm/internal-module "_set_render_options" flags dpr) - - ;; Configurable parameters. - (wasm-set-param-from-route-params-if-present :antialias_threshold) - (wasm-set-param-from-route-params-if-present :viewport_interest_area_threshold) - (wasm-set-param-from-route-params-if-present :max_blocking_time_ms) - (wasm-set-param-from-route-params-if-present :node_batch_threshold) - (wasm-set-param-from-route-params-if-present :blur_downscale_threshold) - - ;; Set browser after `_init`; mark live before sizing so the + ;; `init-context` already ran `_init` through `_set_browser` in + ;; order. Publish the handles, then mark live before sizing so the ;; guarded `resize-*` helpers can run (they require `wasm/live?`, ;; which is true here even while `reloading?` still blocks app callers). - (h/call wasm/internal-module "_set_browser" browser) - - ;; Add event listeners for WebGL context lost (set! wasm/canvas canvas) + ;; Add event listeners for WebGL context lost (when can-listen? (.addEventListener canvas "webglcontextlost" webgl-context-lost-listener) (.addEventListener canvas "webglcontextrestored" webgl-context-restored-listener)) @@ -2587,19 +2538,18 @@ ;; Ensure the WebGL context is properly disposed so browsers do not keep ;; accumulating active contexts between page switches. - (when-let [gl (unchecked-get wasm/internal-module "GL")] - (when-let [handle wasm/gl-context-handle] - (try - ;; For hard teardown we can explicitly lose browser context. - ;; For reload->reinit flows we skip this because immediate context - ;; recreation may fail on some browsers/GPUs while context is lost. - (when lose-browser-context? - (when-let [ctx wasm/gl-context] - (when-let [lose-ext (.getExtension ^js ctx "WEBGL_lose_context")] - (.loseContext ^js lose-ext)))) - (.deleteContext ^js gl handle) - (catch :default dispose-error - (.error js/console dispose-error))))) + (when-let [handle wasm/gl-context-handle] + (try + ;; For hard teardown we can explicitly lose browser context. + ;; For reload->reinit flows we skip this because immediate context + ;; recreation may fail on some browsers/GPUs while context is lost. + (when lose-browser-context? + (when-let [ctx wasm/gl-context] + (when-let [lose-ext (.getExtension ^js ctx "WEBGL_lose_context")] + (.loseContext ^js lose-ext)))) + (webgl/delete-context! wasm/internal-module handle) + (catch :default dispose-error + (.error js/console dispose-error)))) (wasm-gesture/reset-after-wasm-reload!) (wasm/reset-context-state!) diff --git a/frontend/src/app/render_wasm/api/webgl.cljs b/frontend/src/app/render_wasm/api/webgl.cljs index 5fedee789f..3fe8a7b0ba 100644 --- a/frontend/src/app/render_wasm/api/webgl.cljs +++ b/frontend/src/app/render_wasm/api/webgl.cljs @@ -5,12 +5,142 @@ ;; Copyright (c) KALEIDOS SUBSIDIARY SL (ns app.render-wasm.api.webgl - "WebGL utilities for pixel capture and rendering" + "WebGL context creation and utilities for pixel capture and rendering" (:require [app.common.logging :as log] + [app.common.math :as mth] + [app.common.render-wasm.helpers :as h] [app.common.render-wasm.wasm :as wasm] [promesa.core :as p])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Context +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def default-context-options + "Shared WebGL2 context attributes" + #js {:antialias false + :depth true + :stencil true + :alpha true + :preserveDrawingBuffer true}) + +(def ^:const ^:private max-surface-size + ;; Must match `gpu_state::MAX_SURFACE_SIZE`. + 8192) + +(defn canvas-css-size + "Return canvas size in CSS pixels. + + - For DOM canvases: use `clientWidth/clientHeight`. + - For OffscreenCanvas: fall back to `width/height` (physical px) converted by DPR." + [canvas dpr] + (let [cw (.-clientWidth ^js canvas) + ch (.-clientHeight ^js canvas)] + (if (and (number? cw) (pos? cw) + (number? ch) (pos? ch)) + [cw ch] + [(/ (.-width ^js canvas) dpr) + (/ (.-height ^js canvas) dpr)]))) + +(defn clamp-physical-size + "Clamp physical pixel dimensions before assigning `canvas.width/height`. + Rust `resize` applies the same cap and syncs the effective DPR from the + real drawing buffer." + [w h] + (let [w (mth/max 1 w) + h (mth/max 1 h) + scale (mth/min 1 (/ max-surface-size w) (/ max-surface-size h))] + [(mth/max 1 (mth/floor (* scale w))) + (mth/max 1 (mth/floor (* scale h)))])) + +(defn delete-context! + "Deletes an Emscripten GL context by handle.Never throws." + [module handle] + (when (some? handle) + (try + ;; We look the method up by string key bc dot access would let `:advanced` + ;; builds rename the Emscripten method + (let [gl (unchecked-get module "GL") + delete-context (unchecked-get gl "deleteContext")] + (when (fn? delete-context) + (.call ^js delete-context gl handle))) + (catch :default _)))) + +(defn release-context! + "Releases renderer state and a WebGL context owned by the caller. + `handle` may be nil if Emscripten registration failed. Each release is + attempted even when an earlier one throws; cleanup must preserve the + original initialization failure. Writes no global lifecycle state." + [module context handle] + (when (some? handle) + (try + (h/call module "_clean_up") + (catch :default _))) + (try + (when-let [ext (.getExtension ^js context "WEBGL_lose_context")] + (.loseContext ^js ext)) + (catch :default _)) + (delete-context! module handle)) + +(defn init-context + "Create a WebGL2 context on `canvas` and run the WASM renderer init sequence. + + Expects all values resolved by the caller. Writes no `wasm/` atoms and installs + no listeners. + + - `opts` has keys `module`, `context-id`, `css-width`, `css-height`, `dpr`, + `flags`, `browser`, and `params`. + - `module` is the Emscripten module object holding `GL`, `_init`, + `_set_render_options`, and `_set_browser`. `context-id` is `\"webgl2\"` + (or `\"fail\"` to force the debug failure path). + - `css-width` and `css-height` are CSS pixel sizes already probed and clamped + to at least 1. + - `params` maps tunable keywords to values (which pass to `_set_*` unchecked): + - `:antialias_threshold` + - `:viewport_interest_area_threshold` + - `:max_blocking_time_ms` + - `:node_batch_threshold` + - `:blur_downscale_threshold` + + Returns `{:context :handle}` or nil when `getContext` returns nil. + On failure, releases acquired resources before rethrowing. On success, + ownership transfers to the caller." + [canvas {:keys [module context-id css-width css-height dpr flags browser params]}] + (let [gl (unchecked-get module "GL") + context (.getContext ^js canvas context-id default-context-options)] + (when-not (nil? context) + (let [handle (volatile! nil)] + (try + (vreset! handle (.registerContext ^js gl context #js {"majorVersion" 2})) + (.makeContextCurrent ^js gl @handle) + + ;; Force the WEBGL_debug_renderer_info extension as emscripten does not enable it + (.getExtension context "WEBGL_debug_renderer_info") + + ;; Initialize Wasm Render Engine + (h/call module "_init" css-width css-height) + (h/call module "_set_render_options" flags dpr) + + ;; Configurable parameters, fixed order to match the editor path. + (when-let [value (:antialias_threshold params)] + (h/call module "_set_antialias_threshold" value)) + (when-let [value (:viewport_interest_area_threshold params)] + (h/call module "_set_viewport_interest_area_threshold" value)) + (when-let [value (:max_blocking_time_ms params)] + (h/call module "_set_max_blocking_time_ms" value)) + (when-let [value (:node_batch_threshold params)] + (h/call module "_set_node_batch_threshold" value)) + (when-let [value (:blur_downscale_threshold params)] + (h/call module "_set_blur_downscale_threshold" value)) + + (h/call module "_set_browser" browser) + {:context context + :handle @handle} + (catch :default cause + (release-context! module context @handle) + (throw cause))))))) + (defn get-webgl-context "Gets the WebGL context from the WASM module" [] @@ -23,6 +153,10 @@ (when current-ctx (.-GLctx ^js current-ctx))))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Bitmap rendering +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn create-webgl-texture-from-image "Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object" [gl image-element] diff --git a/frontend/test/frontend_tests/render_wasm/serialization_test.cljs b/frontend/test/frontend_tests/render_wasm/serialization_test.cljs index cb5438d1ce..b204bc13b6 100644 --- a/frontend/test/frontend_tests/render_wasm/serialization_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/serialization_test.cljs @@ -5,7 +5,7 @@ ;; Copyright (c) KALEIDOS SUBSIDIARY SL (ns frontend-tests.render-wasm.serialization-test - "Routing tests for `serialize-shape!` and `serialize-shapes-batch!`. + "Routing tests for `serialize-shape` and `serialize-shapes-batch`. A shared `tail-cases` table states, per shape, whether the svg-attrs/path tail must fire. The pure `needs-shape-tail?` predicate is tested against @@ -23,7 +23,7 @@ (defn- with-ffi-stubs* "Stubs the single-arity FFI boundary fns (`flush-shapes-batch!`, - `use-shape!`, `set-shape-svg-attrs`, `set-shape-path-content`). + `use-shape`, `set-shape-svg-attrs`, `set-shape-path-content`). `set-shape-upload!` is deliberately left real: it is a trivial one-liner over the stubbed flush, so the single test exercises its actual delegation and default opts. A plain fn `set!` onto a multi-arity var breaks under @@ -121,7 +121,7 @@ :svg-attrs (fn [attrs] (swap! events conj [:svg-attrs @current attrs]) nil) :path (fn [content] (swap! events conj [:path @current content]) nil)} result (with-ffi-stubs* stubs - #(serialize-shape/serialize-shapes-batch! shapes opts)) + #(serialize-shape/serialize-shapes-batch shapes opts)) flushed (first @flush-calls)] (t/is (= 1 (count @flush-calls)) "exactly one flush") (t/is (= opts (:opts flushed)) "flush carries passed opts") @@ -140,7 +140,7 @@ :svg-attrs (fn [attrs] (swap! svg-applied conj attrs) nil) :path (fn [content] (swap! path-applied conj content) nil)}] (let [expected (svg-derived/apply-svg-derived shape) - result (with-ffi-stubs* stubs #(serialize-shape/serialize-shape! shape))] + result (with-ffi-stubs* stubs #(serialize-shape/serialize-shape shape))] (t/is (= [{:shapes [expected] :opts {:include-layout? false}}] @flush-calls) (str "single upload carries derived shape without layout for " (:type shape))) (t/is (= expected result) @@ -161,7 +161,7 @@ :svg-attrs (fn [_] (t/is false "no svg-attrs write on empty batch") nil) :path (fn [_] (t/is false "no path write on empty batch") nil)} result (with-ffi-stubs* stubs - #(serialize-shape/serialize-shapes-batch! [] opts))] + #(serialize-shape/serialize-shapes-batch [] opts))] (t/is (= [] result) "returns empty prepared") (t/is (empty? @flush-calls) "no flush on empty batch") (t/is (empty? @selected) "no select on empty batch"))) diff --git a/frontend/test/frontend_tests/render_wasm/webgl_test.cljs b/frontend/test/frontend_tests/render_wasm/webgl_test.cljs new file mode 100644 index 0000000000..beb60abce9 --- /dev/null +++ b/frontend/test/frontend_tests/render_wasm/webgl_test.cljs @@ -0,0 +1,105 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.render-wasm.webgl-test + "Unit tests for `app.render-wasm.api.webgl` context init and release." + (:require + [app.render-wasm.api.webgl :as webgl] + [cljs.test :as t :include-macros true])) + +(defn- fake-env + "Builds `{canvas context module calls}` fakes. + - `calls` records `:register`, `:delete`, `:clean` and `:lose` events. + - Options: + - `:throw-in` makes that module fn throw (`:_init`, `:_set_browser`, ...), + - `:register-throws?` fails Emscripten registration (no handle exists), + - `:nil-context?` makes `getContext` return nil (nothing is acquired)." + [{:keys [throw-in register-throws? nil-context?]}] + (let [calls (atom []) + lose (fn [] (swap! calls conj [:lose]) nil) + context #js {:getExtension (fn [name] + (when (= name "WEBGL_lose_context") + #js {:loseContext lose}))} + gl #js {:registerContext (fn [_ _] + (swap! calls conj [:register]) + (when register-throws? + (throw (js/Error. "register failed"))) + 7) + :makeContextCurrent (fn [_] nil) + :deleteContext (fn [handle] + (swap! calls conj [:delete handle]) + nil)} + thrower (fn [name] + (fn [& _] + (when (= name throw-in) + (throw (js/Error. (str "fake " (pr-str name) " failure")))) + nil)) + module #js {:GL gl + :_init (thrower :_init) + :_set_render_options (thrower :_set_render_options) + :_set_browser (thrower :_set_browser) + :_clean_up (fn [& _] (swap! calls conj [:clean]) nil)} + canvas #js {:getContext (fn [_ _] (when-not nil-context? context))}] + {:calls calls :canvas canvas :context context :module module})) + +(defn- init-opts + [module] + {:module module + :context-id "webgl2" + :css-width 100 + :css-height 100 + :dpr 1 + :flags 0 + :browser 0 + :params {}}) + +(defn- init-failure + "Runs `init-context` against `env`, returning the thrown cause." + [{:keys [canvas module]}] + (try + (webgl/init-context canvas (init-opts module)) + nil + (catch :default cause + cause))) + +(defn- effect-count + [calls effect] + (count (filter #(= effect (first %)) calls))) + +(t/deftest init-failure-releases-the-acquired-context + (let [{:keys [canvas module calls]} (fake-env {:throw-in :_set_browser}) + failure (init-failure {:canvas canvas :module module})] + (t/is (some? failure) "the original failure propagates") + (t/is (= "_set_browser" (:fn (ex-data failure))) + "the cause keeps the failing fn") + (t/is (= 1 (effect-count @calls :clean)) "renderer state is released") + (t/is (some #{[:delete 7]} @calls) "the registered handle is deleted") + (t/is (some #{[:lose]} @calls) "the browser context is released"))) + +(t/deftest init-success-returns-context-and-handle + (let [{:keys [canvas context module calls]} (fake-env {}) + result (webgl/init-context canvas (init-opts module))] + (t/is (= 7 (:handle result))) + (t/is (identical? context (:context result))) + (t/is (zero? (effect-count @calls :clean)) "no release on success") + (t/is (zero? (effect-count @calls :delete)) "no release on success") + (t/is (zero? (effect-count @calls :lose)) "no release on success"))) + +(t/deftest nil-context-returns-nil-without-releasing + (let [{:keys [canvas module calls]} (fake-env {:nil-context? true})] + (t/is (nil? (webgl/init-context canvas (init-opts module)))) + (t/is (empty? @calls) "nothing acquired, nothing released"))) + +(t/deftest registration-failure-releases-browser-context-only + (let [{:keys [canvas module calls]} (fake-env {:register-throws? true}) + failure (init-failure {:canvas canvas :module module})] + (t/is (some? failure) "the original failure propagates") + (t/is (zero? (effect-count @calls :clean)) + "no renderer state exists without registration") + (t/is (zero? (effect-count @calls :delete)) + "no handle exists to delete") + (t/is (= 1 (effect-count @calls :lose)) + "the browser context is still released"))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index daaae968a0..df7409c045 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -90,6 +90,7 @@ [frontend-tests.render-wasm.text-editor-apply-styles-test] [frontend-tests.render-wasm.text-editor-caret-color-test] [frontend-tests.render-wasm.text-paste-test] + [frontend-tests.render-wasm.webgl-test] [frontend-tests.router-test] [frontend-tests.svg-fills-test] [frontend-tests.svg-filters-test] @@ -239,6 +240,7 @@ 'frontend-tests.render-wasm.text-editor-apply-styles-test 'frontend-tests.render-wasm.text-editor-caret-color-test 'frontend-tests.render-wasm.text-paste-test + 'frontend-tests.render-wasm.webgl-test 'frontend-tests.router-test 'frontend-tests.svg-fills-test 'frontend-tests.svg-filters-test From cb1118f9045c83784008e2d8ae112abbb3f99320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 7 Oct 2026 18:20:03 +0200 Subject: [PATCH 075/145] :tada: Preserve Penpot's internal formatting when pasting (v3) (#12137) * :tada: Copy v3 editor text with its styles in a payload Copy and cut in the v3 text editor put the styled selection in a hidden span of text/html. Add its codec and the clean-up for pasting into another file or team. Refs #12047 AI-assisted-by: claude-opus-5-5 * :tada: Paste text copied in Penpot at the v3 editor caret Text copied in Penpot keeps its styles when pasted at the caret. The first paragraph keeps the target's attrs; later ones bring their own. The context menu paste now shares the keyboard paste path. Refs #12047 AI-assisted-by: claude-opus-5-5 * :tada: Paste text copied in Penpot on the canvas with its styles Refs #12047 AI-assisted-by: claude-opus-5-5 * :tada: Paste copied text shapes at the v3 editor caret with styles Copied shapes now record their team. Shapes that are not texts paste nothing into the editor. Refs #12047 AI-assisted-by: claude-opus-5-5 * :sparkles: Replace custom fonts in shapes pasted across teams Refs #12047 AI-assisted-by: claude-opus-5-5 --- .../app/main/data/workspace/clipboard.cljs | 27 ++- .../src/app/main/data/workspace/texts.cljs | 189 +++++++++++++++--- .../ui/workspace/shapes/text/v3_editor.cljs | 136 ++----------- frontend/src/app/render_wasm/api.cljs | 10 +- frontend/src/app/render_wasm/text_editor.cljs | 66 ++++++ frontend/src/app/render_wasm/text_paste.cljs | 38 +++- frontend/src/app/util/clipboard.cljs | 46 ++++- frontend/src/app/util/text/clipboard.cljs | 101 +++++++++- .../text_editor_content_range_test.cljs | 56 ++++++ .../render_wasm/text_paste_test.cljs | 46 +++++ frontend/test/frontend_tests/runner.cljs | 2 + .../util_text_clipboard_test.cljs | 93 +++++++++ 12 files changed, 638 insertions(+), 172 deletions(-) create mode 100644 frontend/test/frontend_tests/render_wasm/text_editor_content_range_test.cljs diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 4d365f0bee..557747d080 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -213,6 +213,7 @@ :features features :version version :file-id file-id + :team-id (:current-team-id state) :selected selected :objects {} :images #{}} @@ -578,6 +579,7 @@ [:features ::sm/set-of-strings] [:version :int] [:file-id ::sm/uuid] + [:team-id {:optional true} ::sm/uuid] [:selected ::sm/set-of-uuid] [:objects [:map-of ::sm/uuid :map]] @@ -883,7 +885,7 @@ (assoc change :index (get map-ids (:old-id change))) change))) - (process-shape [valid-file-ids frame-id parent-id shape] + (process-shape [valid-file-ids other-team? frame-id parent-id shape] (cond-> shape :always (assoc :frame-id frame-id :parent-id parent-id) @@ -894,7 +896,11 @@ (assoc :shapes []) (cfh/text-shape? shape) - (ctt/remove-external-typographies valid-file-ids)))] + (ctt/remove-external-typographies valid-file-ids) + + ;; Custom font ids differ between teams. + (and other-team? (cfh/text-shape? shape)) + (update :content text-clipboard/replace-custom-fonts)))] (ptk/reify ::paste-shapes ptk/WatchEvent @@ -947,7 +953,11 @@ valid-file-ids (conj (set (keys libraries)) file-id) - objects (update-vals objects (partial process-shape valid-file-ids frame-id parent-id)) + ;; Shapes copied before the team was recorded count as same team. + other-team? (and (some? (:team-id pdata)) + (not= (:team-id pdata) (:current-team-id state))) + + objects (update-vals objects (partial process-shape valid-file-ids other-team? frame-id parent-id)) all-objects (merge page-objects objects) @@ -1067,10 +1077,13 @@ (watch [_ state _] (let [[text content] (if (v3-html-paste? state) - (when-let [fragment (some-> (text-clipboard/html->fragment html) - (text-clipboard/without-overrides))] - [(text-paste/fragment->text fragment) - (text-paste/fragment->content fragment (txt/get-default-text-attrs))]) + (if-let [payload (text-clipboard/html->payload html)] + (let [content (dwtxt/payload-content state payload)] + [(txt/content->text content) content]) + (when-let [fragment (some-> (text-clipboard/html->fragment html) + (text-clipboard/without-overrides))] + [(text-paste/fragment->text fragment) + (text-paste/fragment->content fragment (txt/get-default-text-attrs))])) (let [style (deref refs/workspace-clipboard-style) root (dwtxt/create-root-from-html html style (features/active-feature? @st/state "text-editor/v2-html-paste"))] [(.-textContent root) (tc/dom->cljs root)]))] diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 4ddaf0eded..1040ec1fd6 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -10,12 +10,14 @@ [app.common.attrs :as attrs] [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.exceptions :as ex] [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.math :as mth] + [app.common.transit :as t] [app.common.types.fills :as types.fills] [app.common.types.modifiers :as ctm] [app.common.types.shape.layout :as ctl] @@ -42,8 +44,10 @@ [app.render-wasm.api :as wasm.api] [app.render-wasm.api.fonts :as wasm.fonts] [app.render-wasm.text-editor :as wasm.text-editor] + [app.render-wasm.text-paste :as text-paste] [app.util.clipboard :as clipboard] [app.util.text-editor :as ted] + [app.util.text.clipboard :as text-clipboard] [app.util.text.content :as tc] [app.util.text.content.styles :as styles] [app.util.timers :as ts] @@ -1419,18 +1423,34 @@ (gsh/transform-shape (ctm/change-size shape width height)))))) {:undo-group (when new-shape? id)})))))))) +(defn- editor-content-update + "Event saving `content` from the WASM editor, renaming the shape after its text." + [shape-id content & {:keys [finalize?]}] + (let [text (txt/content->text content) + name (when (not= text "") + (txt/generate-shape-name text))] + (v2-update-text-shape-content shape-id content + :update-name? true + :name name + :finalize? finalize?))) + (defn v3-sync-editor-content "Event pushing the WASM editor content back into the shape, or nil when there is nothing to sync. Every text edit commits through it, menu or keystroke alike." [& {:keys [finalize?]}] (when-let [{:keys [shape-id content]} (wasm.text-editor/text-editor-sync-content)] - (let [text (txt/content->text content) - name (when (not= text "") - (txt/generate-shape-name text))] - (v2-update-text-shape-content shape-id content - :update-name? true - :name name - :finalize? finalize?)))) + (editor-content-update shape-id content :finalize? finalize?))) + +(defn v3-apply-pending-caret-styles + "Event saving an insertion made at `before` that consumed the pending caret style: + the inserted text gets that style as its own span. Nil when nothing changed." + [shape-id before] + (let [range (wasm.text-editor/typed-range before (wasm.text-editor/caret-position))] + ;; Sync first so the cached content stays index-aligned with WASM. + (wasm.text-editor/text-editor-sync-content) + (if-let [{:keys [content]} (wasm.api/apply-pending-caret-styles! shape-id range)] + (editor-content-update shape-id content) + (v3-sync-editor-content)))) (defn- sync-editor-content-stream "Stream of the sync event for `reason`, after asking WASM to repaint." @@ -1441,19 +1461,23 @@ (rx/of event) (rx/empty)))) -(defn- editor-selected-text - "Plain text of the current WASM editor selection, or nil when there is none." - [] +(defn editor-selection-clipboard-data + "Clipboard items for the WASM editor selection, or nil when nothing is selected. + The HTML also carries the styled content for pasting it in Penpot." + [state] (when (and (wasm.text-editor/text-editor-has-focus?) (wasm.text-editor/text-editor-has-selection?)) (let [text (wasm.text-editor/text-editor-export-selection)] - (when (seq text) text)))) - -(defn- write-selection-to-clipboard - "Write `text` as plain text and HTML; Windows apps often prefer CF_HTML." - [text] - (clipboard/to-clipboard-multi {"text/plain" text - "text/html" (clipboard/plain-text->html text)})) + (when (seq text) + (let [content (when (features/active-feature? state "text-editor-wasm/v1-html-paste") + (-> (wasm.text-editor/text-editor-get-active-shape-id) + (wasm.text-editor/selection-content))) + payload (when (some? content) + (text-clipboard/payload->html content + (:current-file-id state) + (:current-team-id state)))] + {"text/plain" text + "text/html" (clipboard/plain-text->html text payload)}))))) (defn- on-clipboard-error [cause] @@ -1471,9 +1495,9 @@ [] (ptk/reify ::v3-copy-selection ptk/WatchEvent - (watch [_ _ _] - (if-let [text (editor-selected-text)] - (->> (rx/from (write-selection-to-clipboard text)) + (watch [_ state _] + (if-let [data (editor-selection-clipboard-data state)] + (->> (rx/from (clipboard/to-clipboard-multi data)) (rx/ignore) (rx/catch on-clipboard-error)) (rx/empty))))) @@ -1483,9 +1507,9 @@ [] (ptk/reify ::v3-cut-selection ptk/WatchEvent - (watch [_ _ _] - (if-let [text (editor-selected-text)] - (->> (rx/from (write-selection-to-clipboard text)) + (watch [_ state _] + (if-let [data (editor-selection-clipboard-data state)] + (->> (rx/from (clipboard/to-clipboard-multi data)) (rx/mapcat (fn [_] ;; Delete only once the text is safely on the clipboard, ;; so a refused clipboard cannot lose the selection. @@ -1494,23 +1518,122 @@ (rx/catch on-clipboard-error)) (rx/empty))))) +;; Bigger HTML is pasted as plain text rather than walked. +(def ^:private max-paste-html-length 1000000) + +(defn payload-content + "The content of a Penpot text `payload` without what the current file cannot reach. + A payload without team counts as coming from the current team." + [state {:keys [content file-id team-id]}] + (let [current-file-id (:current-file-id state)] + (text-clipboard/clean-content + content + {:valid-file-ids (conj (set (keys (dsh/lookup-libraries state))) current-file-id) + :same-file? (= file-id current-file-id) + :same-team? (or (nil? team-id) (= team-id (:current-team-id state)))}))) + +(defn- decode-copied-shapes + "The copied shapes data in clipboard `text`, or nil when it holds something else." + [text] + (when (str/starts-with? (str/trim (or text "")) "{") + (let [data (ex/ignoring (t/decode-str text))] + (when (and (map? data) (= :copied-shapes (:type data))) + data)))) + +(defn- copied-shapes->payload + "A text payload joining the content of the copied text shapes, top to bottom and + left to right; nil when none of the copied shapes is a text." + [{:keys [objects selected file-id team-id]}] + (let [texts (->> selected + (keep #(get objects %)) + (filter cfh/text-shape?) + (sort-by (juxt :y :x)))] + (when (seq texts) + {:file-id file-id + :team-id team-id + :content (assoc-in (:content (first texts)) + [:children 0 :children] + (into [] (mapcat #(-> % :content :children first :children)) texts))}))) + +(defn- penpot-paste + [state payload] + {:penpot? true + :fragment (text-paste/content->fragment (payload-content state payload))}) + +(defn- clipboard->paste + "What to paste for clipboard `html` and `text`, or nil: text and text shapes copied + in Penpot keep their styles; any other text takes the caret style." + [state html text] + (let [html-paste? (features/active-feature? state "text-editor-wasm/v1-html-paste") + payload (when html-paste? (text-clipboard/html->payload html)) + shapes (when (and html-paste? (nil? payload)) (decode-copied-shapes text))] + (cond + (some? payload) + (penpot-paste state payload) + + (some? shapes) + (some->> (copied-shapes->payload shapes) (penpot-paste state)) + + :else + (when-let [fragment (or (when (and html-paste? (< 0 (count html) max-paste-html-length)) + (some-> (text-clipboard/html->fragment html) + (text-clipboard/without-overrides))) + (text-clipboard/text->fragment text))] + {:penpot? false + :fragment fragment})))) + +(defn- insert-fragment + "Insert the text of `fragment` at the caret and restyle it: with its own styles + or overrides, else with the pending caret style. Returns the event saving it." + [{:keys [fragment penpot?]}] + (let [shape-id (wasm.text-editor/text-editor-get-active-shape-id) + start (wasm.text-editor/selection-start) + styled? (or penpot? (text-paste/styled? fragment)) + pending? (some? (wasm.text-editor/get-pending-caret-styles shape-id))] + (when styled? + (wasm.text-editor/clear-pending-caret-styles!)) + (wasm.text-editor/text-editor-insert-text (text-paste/fragment->text fragment)) + (cond + (nil? start) + (v3-sync-editor-content) + + styled? + (do + ;; Sync first so the cached content stays index-aligned with WASM. + (wasm.text-editor/text-editor-sync-content) + (if-let [{:keys [content]} (wasm.api/apply-paste-styles shape-id fragment start penpot?)] + (editor-content-update shape-id content) + (v3-sync-editor-content))) + + pending? + (v3-apply-pending-caret-styles shape-id start) + + :else + (v3-sync-editor-content)))) + +(defn v3-paste + "Paste clipboard `html` and `text` at the caret, replacing the selection." + [html text] + (ptk/reify ::v3-paste + ptk/WatchEvent + (watch [_ state _] + (if-let [paste (when (wasm.text-editor/text-editor-has-focus?) + (clipboard->paste state html text))] + (let [event (insert-fragment paste)] + (wasm.api/request-render-preserving-target "text-paste") + (if (some? event) (rx/of event) (rx/empty))) + (rx/empty))))) + (defn v3-paste-text - "Insert the system clipboard text at the caret, replacing the selection." + "Paste the system clipboard at the caret, replacing the selection." [] (ptk/reify ::v3-paste-text ptk/WatchEvent (watch [_ _ _] (if-not (wasm.text-editor/text-editor-has-focus?) (rx/empty) - (->> (rx/from (clipboard/read-text)) - (rx/mapcat (fn [text] - (if (seq text) - (do - ;; Pasted text keeps the surrounding style. - (wasm.text-editor/clear-pending-caret-styles!) - (wasm.text-editor/text-editor-insert-text text) - (sync-editor-content-stream "text-paste")) - (rx/empty)))) + (->> (rx/from (clipboard/read-html-and-text)) + (rx/map (fn [{:keys [html text]}] (v3-paste html text))) (rx/catch on-clipboard-error)))))) (defn v3-select-all diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs index f3138a5238..b99b866d6d 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs @@ -9,23 +9,18 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] - [app.common.types.text :as txt] [app.config :as cf] [app.main.data.helpers :as dsh] [app.main.data.workspace :as dw] [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.undo :as dwu] - [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.css-cursors :as cur] [app.render-wasm.api :as wasm.api] [app.render-wasm.text-editor :as text-editor] - [app.render-wasm.text-paste :as text-paste] - [app.util.clipboard :as clipboard] [app.util.dom :as dom] [app.util.keyboard :as kbd] - [app.util.text.clipboard :as text-clipboard] [app.util.timers :as ts] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -63,97 +58,19 @@ "Home" "End" "PageUp" "PageDown" "Enter" "Backspace" "Delete" "Escape" "Tab"}) -(defn- caret-position - "Collapsed caret as {:para :offset} from the WASM selection, or nil." - [] - (when-let [{:keys [focus-para focus-offset]} (text-editor/text-editor-get-selection)] - {:para focus-para :offset focus-offset})) - -(defn- typed-range - "Normalized range covering the text inserted between `before` and `after`, or nil." - [before after] - (when (and before after) - (if (or (< (:para before) (:para after)) - (and (= (:para before) (:para after)) - (<= (:offset before) (:offset after)))) - {:start-para (:para before) :start-offset (:offset before) - :end-para (:para after) :end-offset (:offset after)} - {:start-para (:para after) :start-offset (:offset after) - :end-para (:para before) :end-offset (:offset before)}))) - -(defn- commit-restyled-content - "Save `content` restyled after an insertion, renaming the shape after its text." - [shape-id content] - (let [text (txt/content->text content) - name (when (not= text "") (txt/generate-shape-name text))] - (st/emit! (dwt/v2-update-text-shape-content - shape-id content - :update-name? true - :name name)))) - (defn- sync-with-pending-caret-styles! - "Commit an insertion that consumed a pending caret style: sync the new text, - then restyle the just-typed `range` into its own span. `before` is the + "Commit an insertion that consumed a pending caret style. `before` is the pre-insert caret." [shape-id before] - (let [range (typed-range before (caret-position))] - ;; Sync first so the cached content stays index-aligned with WASM. - (text-editor/text-editor-sync-content) - (if-let [{:keys [content]} (wasm.api/apply-pending-caret-styles! shape-id range)] - (commit-restyled-content shape-id content) - (sync-wasm-text-editor-content!)))) + (when-let [event (dwt/v3-apply-pending-caret-styles shape-id before)] + (st/emit! event))) -;; Bigger HTML is pasted as plain text rather than walked. -(def ^:private max-paste-html-length 1000000) - -(defn- clipboard->fragment - "The paste fragment for `data`: its HTML when allowed and it has text, else its - plain text. Either way it is unstyled, so it adopts the caret style." - [^js data html-paste?] - (let [html (when html-paste? (.getData data "text/html"))] - (or (when (< 0 (count html) max-paste-html-length) - (some-> (text-clipboard/html->fragment html) - (text-clipboard/without-overrides))) - (text-clipboard/text->fragment (.getData data "text/plain"))))) - -(defn- selection-start - "Start of the WASM selection as {:para :offset}: where pasted text goes." - [] - (when-let [{:keys [anchor-para anchor-offset focus-para focus-offset]} - (text-editor/text-editor-get-selection)] - (if (or (< anchor-para focus-para) - (and (= anchor-para focus-para) (<= anchor-offset focus-offset))) - {:para anchor-para :offset anchor-offset} - {:para focus-para :offset focus-offset}))) - -(defn- paste-fragment - "Insert the text of `fragment`, then restyle it with the fragment overrides or, - when it has none, with the pending caret style." - [fragment] - (let [shape-id (text-editor/text-editor-get-active-shape-id) - start (selection-start) - styled? (text-paste/styled? fragment) - pending? (some? (text-editor/get-pending-caret-styles shape-id))] - (when styled? - (text-editor/clear-pending-caret-styles!)) - (text-editor/text-editor-insert-text (text-paste/fragment->text fragment)) - (cond - (nil? start) - (sync-wasm-text-editor-content!) - - styled? - (do - ;; Sync first so the cached content stays index-aligned with WASM. - (text-editor/text-editor-sync-content) - (if-let [{:keys [content]} (wasm.api/apply-paste-styles shape-id fragment start)] - (commit-restyled-content shape-id content) - (sync-wasm-text-editor-content!))) - - pending? - (sync-with-pending-caret-styles! shape-id start) - - :else - (sync-wasm-text-editor-content!)))) +(defn- write-clipboard-items + "Write `items` (mime type -> string) to the DataTransfer of a copy or cut event. + Windows apps prefer text/html, or they may paste the editor's empty `
`." + [^js data items] + (doseq [[mime value] items] + (.setData data mime value))) (defn- reset-input-node "Empties the contenteditable capture surface and restores a collapsed caret @@ -331,11 +248,9 @@ (mf/use-fn (fn [^js event] (dom/prevent-default event) - (when-let [fragment (some-> (.-clipboardData event) - (clipboard->fragment - (features/active-feature? @st/state "text-editor-wasm/v1-html-paste")))] - (paste-fragment fragment) - (wasm.api/request-render-preserving-target "text-paste")) + (when-let [data (.-clipboardData event)] + (st/emit! (dwt/v3-paste (.getData data "text/html") + (.getData data "text/plain")))) (reset-input-node (mf/ref-val contenteditable-ref)))) on-copy @@ -343,30 +258,19 @@ (fn [^js event] (when (text-editor/text-editor-has-focus?) (dom/prevent-default event) - (when (text-editor/text-editor-has-selection?) - (let [text (or (text-editor/text-editor-export-selection) "") - html (clipboard/plain-text->html text) - data (.-clipboardData event)] - ;; text/html matters on Windows: many apps prefer CF_HTML, and - ;; without it they can pick up the empty contenteditable `
`. - (.setData data "text/plain" text) - (.setData data "text/html" html)))))) + (when-let [items (dwt/editor-selection-clipboard-data @st/state)] + (write-clipboard-items (.-clipboardData event) items))))) on-cut (mf/use-fn (fn [^js event] (when (text-editor/text-editor-has-focus?) (dom/prevent-default event) - (when (text-editor/text-editor-has-selection?) - (let [text (or (text-editor/text-editor-export-selection) "") - html (clipboard/plain-text->html text) - data (.-clipboardData event)] - (.setData data "text/plain" text) - (.setData data "text/html" html) - (when (seq text) - (text-editor/text-editor-delete-backward) - (sync-wasm-text-editor-content!) - (wasm.api/request-render-preserving-target "text-cut")))) + (when-let [items (dwt/editor-selection-clipboard-data @st/state)] + (write-clipboard-items (.-clipboardData event) items) + (text-editor/text-editor-delete-backward) + (sync-wasm-text-editor-content!) + (wasm.api/request-render-preserving-target "text-cut")) (reset-input-node (mf/ref-val contenteditable-ref))))) on-key-down @@ -515,7 +419,7 @@ (let [shape-id (text-editor/text-editor-get-active-shape-id) ;; The inserted character adopts a pending caret style, if any. pending-styles? (some? (text-editor/get-pending-caret-styles shape-id)) - before (when pending-styles? (caret-position))] + before (when pending-styles? (text-editor/caret-position))] (text-editor/text-editor-insert-text data) (if pending-styles? (sync-with-pending-caret-styles! shape-id before) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 93f840767e..d85571e1f0 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -805,11 +805,13 @@ result))) (defn apply-paste-styles - "Restyle the text just pasted at `start` with the overrides of `fragment`; - returns {:shape-id :content}, or nil when the shape has no cached content." - [shape-id fragment start] + "Restyle the text just pasted at `start` with `fragment`: its overrides, or its full + styles when copied in Penpot. Nil when the shape has no cached content." + [shape-id fragment start penpot?] (when-let [content (text-editor/get-cached-content shape-id)] - (let [content (text-paste/apply-fragment-styles content fragment start)] + (let [content (if penpot? + (text-paste/apply-content-styles content fragment start) + (text-paste/apply-fragment-styles content fragment start))] (wselect/use-shape shape-id) (set-shape-text-content shape-id content) (request-render "apply-paste-styles") diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index 6493128aba..eff80ba610 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -670,6 +670,26 @@ {:start-para focus-para :start-offset focus-offset :end-para anchor-para :end-offset anchor-offset})) +(defn caret-position + "Collapsed caret as {:para :offset} from the WASM selection, or nil." + [] + (when-let [{:keys [focus-para focus-offset]} (text-editor-get-selection)] + {:para focus-para :offset focus-offset})) + +(defn selection-start + "Start of the WASM selection as {:para :offset}: where inserted text goes." + [] + (when-let [selection (text-editor-get-selection)] + (let [{:keys [start-para start-offset]} (normalize-selection selection)] + {:para start-para :offset start-offset}))) + +(defn typed-range + "Normalized range covering the text inserted between `before` and `after`, or nil." + [before after] + (when (and before after) + (normalize-selection {:anchor-para (:para before) :anchor-offset (:offset before) + :focus-para (:para after) :focus-offset (:offset after)}))) + (defn apply-attrs-to-paragraph "Apply `styles` (attrs map, or a fn per span) within [sel-start, sel-end), splitting spans." [para sel-start sel-end styles] @@ -772,6 +792,52 @@ paragraphs)] (assoc content :children [(assoc paragraph-set :children new-paragraphs)]))) +(defn- slice-paragraph + "`para` with only the text in [start, end), cutting the spans at both ends. + With no text left it keeps its first span, empty, for the style." + [para start end] + (let [spans (loop [spans (:children para) + pos 0 + acc []] + (if (empty? spans) + acc + (let [span (first spans) + text (:text span) + span-end (+ pos (count text)) + ol-start (max pos start) + ol-end (min span-end end)] + (recur (rest spans) span-end + (cond-> acc + (< ol-start ol-end) + (conj (assoc span :text (subs text (- ol-start pos) (- ol-end pos)))))))))] + (assoc para :children (if (seq spans) + spans + [(assoc (first (:children para)) :text "")])))) + +(defn content-range + "The part of `content` inside the char range, with its paragraph and span styles." + [content {:keys [start-para start-offset end-para end-offset]}] + (let [paragraph-set (first (:children content)) + paragraphs (->> (:children paragraph-set) + (map-indexed vector) + (keep (fn [[idx para]] + (when (<= start-para idx end-para) + (slice-paragraph para + (if (= idx start-para) start-offset 0) + (if (= idx end-para) end-offset (para-char-count para)))))) + (vec))] + (assoc content :children [(assoc paragraph-set :children paragraphs)]))) + +(defn selection-content + "The content of the editor selection in `shape-id`, or nil when it is collapsed." + [shape-id] + (when-let [selection (text-editor-get-selection)] + (let [{:keys [start-para start-offset end-para end-offset] :as range} + (normalize-selection selection)] + (when-let [content (get-cached-content shape-id)] + (when-not (and (= start-para end-para) (= start-offset end-offset)) + (content-range content range)))))) + (defn- clean-styles "Drop nil-valued attrs (unlike the DOM path, our merge would keep them and fail the backend schema); a per-span fn is passed through untouched." diff --git a/frontend/src/app/render_wasm/text_paste.cljs b/frontend/src/app/render_wasm/text_paste.cljs index afb72674f6..dbdd6cffec 100644 --- a/frontend/src/app/render_wasm/text_paste.cljs +++ b/frontend/src/app/render_wasm/text_paste.cljs @@ -6,8 +6,9 @@ (ns app.render-wasm.text-paste "Restyles the text WASM just inserted from a paste fragment (see - `app.util.text.clipboard`) with each run's overrides." + `app.util.text.clipboard`), with run overrides or full Penpot styles." (:require + [app.common.types.text :as txt] [app.main.fonts :as fonts] [app.render-wasm.text-editor :as text-editor] [cuerdas.core :as str])) @@ -90,3 +91,38 @@ (text-editor/apply-styles-over-range content range #(resolve-overrides % attrs))) content (styled-ranges fragment start))) + +;; --- Text copied in Penpot + +(def ^:private style-attrs + (into txt/paragraph-attrs txt/text-node-attrs)) + +(defn content->fragment + "The paste fragment for Penpot `content`. Its attrs are whole paragraph and span + styles, not overrides." + [content] + (->> (-> content :children first :children) + (mapv (fn [paragraph] + {:attrs (dissoc paragraph :type :children) + :children (into [] + (comp (filter (comp seq :text)) + (map (fn [span] {:text (:text span) :attrs (dissoc span :text)}))) + (:children paragraph))})))) + +(defn- restyle + "`node` with its style replaced by `attrs`." + [node attrs] + (merge (apply dissoc node style-attrs) attrs)) + +(defn apply-content-styles + "Restyles the Penpot `fragment` text WASM inserted at `start` in `content`. The + first paragraph keeps the style of the one it went into; the others bring their own." + [content fragment start] + (let [content (reduce (fn [content {:keys [range attrs]}] + (text-editor/apply-styles-over-range content range #(restyle % attrs))) + content + (styled-ranges fragment start))] + (reduce (fn [content [idx {:keys [attrs]}]] + (update-in content [:children 0 :children (+ (:para start) idx)] restyle attrs)) + content + (rest (map-indexed vector fragment))))) diff --git a/frontend/src/app/util/clipboard.cljs b/frontend/src/app/util/clipboard.cljs index 3199a6f093..1f0b7decda 100644 --- a/frontend/src/app/util/clipboard.cljs +++ b/frontend/src/app/util/clipboard.cljs @@ -24,16 +24,14 @@ :allowHTMLPaste false}) (defn plain-text->html - "Build a minimal text/html clipboard payload from plain text. - - Windows apps often prefer CF_HTML over CF_UNICODETEXT; writing only - text/plain from a contenteditable copy handler can leave them with the - editor surface's empty/`
` fallback (a lone newline)." - [text] - (let [escaped (-> (or text "") - (dom/escape-html) - (str/replace "\n" "
"))] - (str "" escaped))) + "Minimal text/html clipboard payload for `text`, with `hidden-html` before it. + Windows apps prefer CF_HTML, and may otherwise paste the editor's empty `
`." + ([text] (plain-text->html text nil)) + ([text hidden-html] + (let [escaped (-> (or text "") + (dom/escape-html) + (str/replace "\n" "
"))] + (str "" hidden-html escaped)))) (defn- from-data-transfer "Get clipboard stream from DataTransfer instance" @@ -225,6 +223,34 @@ (.readText ^js clipboard) (unavailable-error)))) +(defn- read-item-type + "Promise of the `mime` text of clipboard `item`, or of nil when it has none." + [^js item mime] + (if (some #{mime} (some-> item .-types array-seq)) + (-> (.getType item mime) (.then #(.text ^js %))) + (js/Promise.resolve nil))) + +(defn read-html-and-text + "Promise of the system clipboard as {:html :text}, either may be nil. Rejects like + `to-clipboard` when the asynchronous Clipboard API is not exposed." + [] + (let [clipboard (get-clipboard)] + (cond + (and clipboard (unchecked-get clipboard "read")) + (-> (.read ^js clipboard) + (.then (fn [items] + (let [item (first items)] + (-> (js/Promise.all #js [(read-item-type item "text/html") + (read-item-type item "text/plain")]) + (.then (fn [[html text]] {:html html :text text}))))))) + + (and clipboard (unchecked-get clipboard "readText")) + (-> (.readText ^js clipboard) + (.then (fn [text] {:html nil :text text}))) + + :else + (unavailable-error)))) + (defn permission-error? "True for the `NotAllowedError` DOMException raised when access is denied." [cause] diff --git a/frontend/src/app/util/text/clipboard.cljs b/frontend/src/app/util/text/clipboard.cljs index 4c806fd1c4..f0f76bee6b 100644 --- a/frontend/src/app/util/text/clipboard.cljs +++ b/frontend/src/app/util/text/clipboard.cljs @@ -6,8 +6,14 @@ (ns app.util.text.clipboard "Reads clipboard data into paste fragments: `[{:attrs {} :children [{:text :attrs}]}]`, - where run attrs are emphasis overrides laid over the style at the caret." + whose run attrs override the caret style; Penpot text also carries a hidden payload." (:require + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.fonts :as cfnt] + [app.common.transit :as t] + [app.common.types.text :as txt] + [app.common.types.typography :as ctt] [cuerdas.core :as str])) (def ^:private block-tags @@ -263,3 +269,96 @@ {:attrs {} :children (if (empty? line) [] [{:text line :attrs {}}])}) (.split (str/replace text "\r" "") "\n")))) + +;; --- Penpot payload +;; +;; Text copied in Penpot keeps its content in the `data-penpot-text` attr of an +;; empty span in the HTML: the one hidden marker every browser keeps. + +(def ^:private payload-re #"data-penpot-text=\"([A-Za-z0-9+/=]*)\"") + +(defn- encode-base64 + [s] + (->> (.encode (js/TextEncoder.) s) + (.from js/Array) + (map #(.fromCharCode js/String %)) + (str/join) + (js/btoa))) + +(defn- decode-base64 + [s] + (->> (.from js/Uint8Array (js/atob s) #(.charCodeAt % 0)) + (.decode (js/TextDecoder.)))) + +(defn payload->html + "Hidden markup carrying `content`, copied from `file-id` in `team-id`." + [content file-id team-id] + (let [payload {:type :copied-text + :version 1 + :file-id file-id + :team-id team-id + :content content}] + (str ""))) + +(defn html->payload + "The Penpot payload in `html`, or nil when it has none." + [html] + (when-let [[_ encoded] (some->> html (re-find payload-re))] + (let [payload (ex/ignoring (t/decode-str (decode-base64 encoded)))] + (when (and (map? payload) + (= :copied-text (:type payload)) + (map? (:content payload))) + payload)))) + +;; --- Pasting into another file or team + +(def ^:private default-font + (d/seek #(= (:id %) (:font-id txt/default-text-attrs)) cfnt/local-fonts)) + +(defn- replace-custom-font + "`node` in the default font, at the closest weight and style it has." + [{:keys [font-weight font-style] :as node}] + (let [variant (or (cfnt/closest-variant (:variants default-font) (str font-weight) font-style) + (d/seek #(= (:id %) (:font-variant-id txt/default-text-attrs)) (:variants default-font)))] + (-> node + (assoc :font-id (:id default-font) + :font-family (:family default-font) + :font-variant-id (:id variant) + :font-weight (:weight variant) + :font-style (:style variant)) + (ctt/remove-typography-from-node)))) + +(defn replace-custom-fonts + "`content` with every custom font replaced by the default one, for pasting it + into another team, where custom fonts have other ids." + [content] + (txt/transform-nodes #(= :custom (cfnt/font-id->backend (:font-id %))) + replace-custom-font + content)) + +(defn- drop-image-fills + "`node` without image fills, or with the default fill when it had only those." + [{:keys [fills] :as node}] + (if (some :fill-image fills) + (let [fills (into [] (remove :fill-image) fills)] + (assoc node :fills (if (seq fills) fills (txt/get-default-text-fills)))) + node)) + +(defn clean-content + "`content` without what the paste target cannot reach: typographies outside + `valid-file-ids`, custom fonts from another team, images from another file." + [content {:keys [valid-file-ids same-team? same-file?]}] + (txt/transform-nodes + (fn [node] + (cond-> node + (and (some? (:typography-ref-file node)) + (not (contains? valid-file-ids (:typography-ref-file node)))) + (ctt/remove-typography-from-node) + + (and (not same-team?) + (= :custom (cfnt/font-id->backend (:font-id node)))) + (replace-custom-font) + + (not same-file?) + (drop-image-fills))) + content)) diff --git a/frontend/test/frontend_tests/render_wasm/text_editor_content_range_test.cljs b/frontend/test/frontend_tests/render_wasm/text_editor_content_range_test.cljs new file mode 100644 index 0000000000..675b1ea18a --- /dev/null +++ b/frontend/test/frontend_tests/render_wasm/text_editor_content_range_test.cljs @@ -0,0 +1,56 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.render-wasm.text-editor-content-range-test + "The styled content of a char range, as copied from the editor selection." + (:require + [app.render-wasm.text-editor :as text-editor] + [cljs.test :as t :include-macros true])) + +(defn- span [text attrs] + (assoc attrs :text text)) + +(defn- paragraph [attrs & spans] + (assoc attrs :type "paragraph" :children (vec spans))) + +(defn- content [& paragraphs] + {:type "root" + :children [{:type "paragraph-set" :children (vec paragraphs)}]}) + +(def ^:private bold {:font-weight "700"}) +(def ^:private red {:fills [{:fill-color "#ff0000" :fill-opacity 1}]}) +(def ^:private centered {:text-align "center"}) +(def ^:private right {:text-align "right"}) + +(def ^:private source + (content (paragraph centered (span "Hello " {}) (span "bold" bold) (span " end" {})) + (paragraph right (span "red" red) (span " tail" {})))) + +(t/deftest single-paragraph + (t/testing "spans are cut at both ends and keep their styles" + (t/is (= (content (paragraph centered (span "lo " {}) (span "bold" bold) (span " e" {}))) + (text-editor/content-range source {:start-para 0 :start-offset 3 + :end-para 0 :end-offset 12}))))) + +(t/deftest across-paragraphs + (t/testing "every paragraph in the range keeps its attrs" + (t/is (= (content (paragraph centered (span "end" {})) + (paragraph right (span "re" red))) + (text-editor/content-range source {:start-para 0 :start-offset 11 + :end-para 1 :end-offset 2})))) + + (t/testing "a range starting at a paragraph end keeps an empty paragraph with its style" + (t/is (= (content (paragraph centered (span "" {})) + (paragraph right (span "red" red))) + (text-editor/content-range source {:start-para 0 :start-offset 14 + :end-para 1 :end-offset 3}))))) + +(t/deftest utf16-offsets + (t/testing "offsets count UTF-16 units like WASM's" + (t/is (= (content (paragraph {} (span "😀b" bold))) + (text-editor/content-range (content (paragraph {} (span "a😀b" bold))) + {:start-para 0 :start-offset 1 + :end-para 0 :end-offset 4}))))) diff --git a/frontend/test/frontend_tests/render_wasm/text_paste_test.cljs b/frontend/test/frontend_tests/render_wasm/text_paste_test.cljs index 7394d031a4..795309aff3 100644 --- a/frontend/test/frontend_tests/render_wasm/text_paste_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/text_paste_test.cljs @@ -144,3 +144,49 @@ (let [result (text-paste/fragment->content [(fragment-paragraph (run "x"))] regular)] (t/is (= (assoc regular :type "paragraph") (dissoc (-> result :children first :children first) :children)))))) + +;; --- Text copied in Penpot + +(def ^:private brand + {:font-id "gfont-roboto" :font-family "Roboto" :font-variant-id "700italic" + :font-weight "700" :font-style "italic" + :fills [{:fill-color "#ff0000" :fill-opacity 1}]}) + +(t/deftest content->fragment + (t/testing "paragraphs and spans keep their whole style, and empty spans are dropped" + (t/is (= [{:attrs {:text-align "center"} + :children [{:text "x" :attrs brand}]} + {:attrs {:text-align "right"} + :children []}] + (text-paste/content->fragment + {:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" :text-align "center" + :children [(assoc brand :text "x") (assoc brand :text "")]} + {:type "paragraph" :text-align "right" + :children [(assoc brand :text "")]}]}]}))))) + +(t/deftest penpot-styles-replace-the-caret-style + (t/testing "pasted runs take their whole style, dropping the target's typography" + (let [pasted (content [(span "AxyB" {:typography-ref-id "t" :typography-ref-file "f"})]) + fragment [(fragment-paragraph (run "x" brand) (run "y" regular))] + result (text-paste/apply-content-styles pasted fragment {:para 0 :offset 1})] + (t/is (= [[(span "A" {:typography-ref-id "t" :typography-ref-file "f"}) + (assoc brand :text "x") + (span "y") + (span "B" {:typography-ref-id "t" :typography-ref-file "f"})]] + (spans-of result))))) + + (t/testing "the first paragraph keeps its style; later ones bring theirs" + (let [pasted {:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" :text-align "left" :children [(span "Ax")]} + {:type "paragraph" :text-align "left" :children [(span "yB")]}]}]} + fragment [{:attrs {:text-align "center"} :children [(run "x" brand)]} + {:attrs {:text-align "right"} :children [(run "y" brand)]}] + result (text-paste/apply-content-styles pasted fragment {:para 0 :offset 1})] + (t/is (= ["left" "right"] + (map :text-align (-> result :children first :children)))) + (t/is (= [[(span "A") (assoc brand :text "x")] + [(assoc brand :text "y") (span "B")]] + (spans-of result)))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index df7409c045..28d2c2419f 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -89,6 +89,7 @@ [frontend-tests.render-wasm.serialization-test] [frontend-tests.render-wasm.text-editor-apply-styles-test] [frontend-tests.render-wasm.text-editor-caret-color-test] + [frontend-tests.render-wasm.text-editor-content-range-test] [frontend-tests.render-wasm.text-paste-test] [frontend-tests.render-wasm.webgl-test] [frontend-tests.router-test] @@ -239,6 +240,7 @@ 'frontend-tests.render-wasm.serialization-test 'frontend-tests.render-wasm.text-editor-apply-styles-test 'frontend-tests.render-wasm.text-editor-caret-color-test + 'frontend-tests.render-wasm.text-editor-content-range-test 'frontend-tests.render-wasm.text-paste-test 'frontend-tests.render-wasm.webgl-test 'frontend-tests.router-test diff --git a/frontend/test/frontend_tests/util_text_clipboard_test.cljs b/frontend/test/frontend_tests/util_text_clipboard_test.cljs index d2d757fbb0..c0bcdbd07f 100644 --- a/frontend/test/frontend_tests/util_text_clipboard_test.cljs +++ b/frontend/test/frontend_tests/util_text_clipboard_test.cljs @@ -9,6 +9,8 @@ survives; the documents come from jsdom since the runner has no DOM." (:require ["jsdom" :refer [JSDOM]] + [app.common.types.text :as txt] + [app.common.uuid :as uuid] [app.util.text.clipboard :as clipboard] [cljs.test :as t :include-macros true])) @@ -174,3 +176,94 @@ (t/testing "empty paragraphs stay empty" (t/is (= [(paragraph)] (clipboard/without-overrides [(paragraph)]))))) + +;; --- Penpot payload + +(def ^:private file-id (uuid/next)) +(def ^:private team-id (uuid/next)) + +(defn- text-content [& spans] + {:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" :text-align "center" :children (vec spans)}]}]}) + +(def ^:private gradient-span + {:text "Ñandú 😀 \"\"" + :font-id "custom-6e2c1a5e-1c1e-4b54-9c64-2a8c5f1a4c11" + :font-family "Brand Sans" + :font-variant-id "bold" + :font-weight "700" + :font-style "italic" + :fills [{:fill-color-gradient {:type :linear :start-x 0 :start-y 0 :end-x 1 :end-y 1 :width 1 + :stops [{:color "#ff0000" :opacity 1 :offset 0} + {:color "#0000ff" :opacity 1 :offset 1}]} + :fill-opacity 1}]}) + +(t/deftest payload-round-trip + (let [content (text-content gradient-span) + hidden (clipboard/payload->html content file-id team-id)] + (t/testing "the payload decodes back to the same content, file and team" + (t/is (= {:type :copied-text :version 1 :file-id file-id :team-id team-id :content content} + (clipboard/html->payload (str "" hidden "Hello"))))) + + (t/testing "it survives the markup browsers add around it" + (let [safari (-> hidden + (.replace ">" " style=\"color: rgb(0, 0, 0); font-size: medium;\">"))] + (t/is (= content + (:content (clipboard/html->payload + (str "" safari "Hello"))))))))) + +(t/deftest html-without-payload + (t/testing "HTML without a payload, or with a broken one, gives nil" + (t/is (nil? (clipboard/html->payload "

Hello

"))) + (t/is (nil? (clipboard/html->payload ""))) + (t/is (nil? (clipboard/html->payload nil))))) + +;; --- Clean-up + +(def ^:private image-fill + {:fill-image {:id (uuid/next) :width 10 :height 10 :mtype "image/png"} :fill-opacity 1}) + +(def ^:private red-fill + {:fill-color "#ff0000" :fill-opacity 1}) + +(defn- spans-of [content] + (-> content :children first :children first :children)) + +(t/deftest clean-content + (let [library-id (uuid/next) + typography {:typography-ref-file library-id :typography-ref-id (uuid/next)} + content (text-content (merge gradient-span typography) + {:text "img" :fills [image-fill red-fill]} + {:text "only img" :fills [image-fill]}) + same {:valid-file-ids #{library-id} :same-team? true :same-file? true}] + + (t/testing "in the same file nothing changes" + (t/is (= content (clipboard/clean-content content same)))) + + (t/testing "typographies from unreachable files are dropped" + (t/is (nil? (-> (clipboard/clean-content content (assoc same :valid-file-ids #{})) + (spans-of) + (first) + :typography-ref-id)))) + + (t/testing "custom fonts from another team become the default font, at the closest variant" + (let [span (first (spans-of (clipboard/clean-content content (assoc same :same-team? false))))] + (t/is (= {:font-id "sourcesanspro" :font-family "sourcesanspro" + :font-variant-id "bolditalic" :font-weight "700" :font-style "italic"} + (select-keys span [:font-id :font-family :font-variant-id :font-weight :font-style]))) + (t/is (nil? (:typography-ref-id span))))) + + (t/testing "builtin and google fonts stay in another team" + (let [content (text-content (assoc gradient-span :font-id "gfont-roboto" :font-family "Roboto"))] + (t/is (= content (clipboard/clean-content content (assoc same :same-team? false)))))) + + (t/testing "images from another file are dropped, leaving the default fill when nothing is left" + (t/is (= [(:fills gradient-span) [red-fill] (txt/get-default-text-fills)] + (map :fills (spans-of (clipboard/clean-content content (assoc same :same-file? false))))))))) + +(t/deftest replace-custom-fonts + (t/testing "only custom fonts become the default font" + (let [content (text-content gradient-span (assoc gradient-span :font-id "gfont-roboto" :font-family "Roboto"))] + (t/is (= ["sourcesanspro" "gfont-roboto"] + (map :font-id (spans-of (clipboard/replace-custom-fonts content)))))))) From b3a62eeb4b19e3e0b5b6a4c04e380cb87cd9bcff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Thu, 8 Oct 2026 09:07:33 +0200 Subject: [PATCH 076/145] :bug: Paint layout children in canvas z-order in vector exports (#12171) --- render-wasm/src/render.rs | 7 ++++ render-wasm/src/render/svg/frames.rs | 4 +- render-wasm/src/render/svg/tests.rs | 62 +++++++++++++++++++++++++++- render-wasm/src/render/vector.rs | 5 ++- 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index cfc3ed1a0f..322319796b 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -349,6 +349,13 @@ fn sort_z_index(tree: ShapesPoolRef, element: &Shape, children_ids: Vec) - } } +/// Children bottom to top, in the same order the GPU renderer paints them. +pub(crate) fn children_paint_order(tree: ShapesPoolRef, element: &Shape) -> Vec { + let mut ids = sort_z_index(tree, element, element.children_ids(false)); + ids.reverse(); + ids +} + /// Whether this `render_shape_exit` pass closes the shape's focus scope. A /// masked group keeps it open across the mask pass. fn exit_closes_focus_scope(element: &Shape, visited_mask: bool) -> bool { diff --git a/render-wasm/src/render/svg/frames.rs b/render-wasm/src/render/svg/frames.rs index 99fede43b3..fa3bb808f6 100644 --- a/render-wasm/src/render/svg/frames.rs +++ b/render-wasm/src/render/svg/frames.rs @@ -9,7 +9,7 @@ use super::document::{ }; use super::images::{emit_fills, emit_strokes}; use super::render_tree; -use crate::render::RenderResources; +use crate::render::{children_paint_order, RenderResources}; pub(super) fn render_frame( builder: &mut SvgLayerCanvas, @@ -104,7 +104,7 @@ fn render_frame_body( )?; } - let children: Vec<_> = element.children_ids_iter_forward(false).copied().collect(); + let children = children_paint_order(tree, element); for child_id in &children { render_tree(builder, shared, child_id, tree, scale)?; } diff --git a/render-wasm/src/render/svg/tests.rs b/render-wasm/src/render/svg/tests.rs index 2f5e5ff8e6..7557eebb7c 100644 --- a/render-wasm/src/render/svg/tests.rs +++ b/render-wasm/src/render/svg/tests.rs @@ -1,8 +1,9 @@ use super::fixtures::*; use crate::shapes::{ - radius_to_sigma, BlendMode, Blur, BlurType, Fill, ImageFill, ImageFillTransform, Shadow, - ShadowStyle, SolidColor, StrokeCap, StrokeKind, + radius_to_sigma, AlignContent, AlignItems, BlendMode, Blur, BlurType, Fill, FlexDirection, + ImageFill, ImageFillTransform, JustifyContent, JustifyItems, Shadow, ShadowStyle, SolidColor, + StrokeCap, StrokeKind, WrapType, }; use crate::state::ShapesPool; use crate::uuid::Uuid; @@ -222,6 +223,63 @@ fn exports_frame_background_blur_before_children() { insta::assert_snapshot!(svg); } +#[test] +fn exports_flex_children_with_first_child_on_top() { + let mut pool = ShapesPool::new(); + let frame_id = uid(1); + let first = uid(2); + let second = uid(3); + add_frame( + &mut pool, + frame_id, + Uuid::nil(), + (0.0, 0.0, 200.0, 120.0), + skia::Color::from_rgb(255, 255, 255), + false, + ); + pool.get_mut(&frame_id).unwrap().set_flex_layout_data( + FlexDirection::Row, + 0.0, + 0.0, + AlignItems::Start, + AlignContent::Start, + JustifyItems::Start, + JustifyContent::Start, + WrapType::NoWrap, + 0.0, + 0.0, + 0.0, + 0.0, + ); + add_solid_rect( + &mut pool, + first, + frame_id, + (20.0, 20.0, 80.0, 80.0), + skia::Color::from_rgb(255, 0, 0), + ); + add_solid_rect( + &mut pool, + second, + frame_id, + (40.0, 40.0, 100.0, 100.0), + skia::Color::from_rgb(0, 0, 255), + ); + { + let frame = pool.get_mut(&frame_id).unwrap(); + frame.add_child(first); + frame.add_child(second); + } + + let svg = render(&pool, frame_id); + let first_pos = svg.find("fill=\"red\"").expect("first child"); + let second_pos = svg.find("fill=\"blue\"").expect("second child"); + assert!( + second_pos < first_pos, + "flex paints its first child on top: {svg}" + ); +} + #[test] fn exports_group_background_blur_selrect_clip() { let mut pool = ShapesPool::new(); diff --git a/render-wasm/src/render/vector.rs b/render-wasm/src/render/vector.rs index c04be04683..15a8dc0c32 100644 --- a/render-wasm/src/render/vector.rs +++ b/render-wasm/src/render/vector.rs @@ -8,6 +8,7 @@ use crate::shapes::{ use crate::state::ShapesPoolRef; use crate::uuid::Uuid; +use super::children_paint_order; use super::shape_renderer::ShapeRenderer; use super::text; use super::RenderResources; @@ -934,7 +935,7 @@ fn render_frame( } // Children (absolute coords, no frame transform). - let children: Vec = element.children_ids_iter_forward(false).copied().collect(); + let children = children_paint_order(tree, element); let child_opts = opts.for_children_of(element); for child_id in &children { render_tree_inner(shared, canvas, child_id, tree, scale, &child_opts)?; @@ -988,7 +989,7 @@ fn render_container_drop_shadows( renderer.draw_fills(element, &element.fills)?; } - let children: Vec = element.children_ids_iter_forward(false).copied().collect(); + let children = children_paint_order(tree, element); let child_opts = opts.for_children_of(element); for child_id in &children { render_tree_inner(shared, canvas, child_id, tree, scale, &child_opts)?; From 41efc3558ad506b38b52e791f5d9d58679d9a4fa Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 8 Oct 2026 09:54:46 +0200 Subject: [PATCH 077/145] :bug: Fix nested libraries vanishing from the libraries modal (#12031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :bug: Fix nested libraries vanishing from the libraries modal The "This file" tab nests each linked library under a linked library that uses it, taken from the team shared files. A library with such a parent was removed from the top level and only drawn under its parent if the parent was at the top level itself. So: - In a chain of two or more levels (file -> A -> B -> C), C was lost. - In a cycle (A uses B and B uses A), both had a parent, none was at the top level, and both were lost. The libraries showed while the shared files were loading, and vanished when they arrived. Build the list as a tree instead: direct libraries and libraries not used by any other linked library are roots, nested libraries follow their parent at any depth, and each library is added only once, even when the dependencies form a cycle. AI-assisted-by: claude-opus-5-5 * :recycle: Move the linked libraries tree to sort-linked-libraries Extract the code that orders the linked libraries as a tree from the file tab to a public function, so it can be tested on its own. Add tests for a cycle with a direct root, a cycle with no root, a deep chain and no dependencies yet. AI-assisted-by: claude-opus-5-5 * :recycle: Use connected-to to find library parents when sorting The caller already computes :connected-to for each linked library, the reverse index of the dependencies map. sort-linked-libraries rebuilt the same relation from dependencies. Read it from :connected-to instead, so the function takes only the libraries and the component memo no longer depends on dependencies. Reword the cycle-without-root test to say it models the unknown direct flag, and add tests for a diamond, a self-dependency, a cycle of three without root, a parent that is not a linked library, and for no library being lost or duplicated. AI-assisted-by: claude-sonnet-5-5 --------- Co-authored-by: Andrés Moya --- .../src/app/main/ui/workspace/libraries.cljs | 91 ++++++++++------ frontend/test/frontend_tests/runner.cljs | 2 + .../ui/workspace_libraries_test.cljs | 101 ++++++++++++++++++ 3 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 frontend/test/frontend_tests/ui/workspace_libraries_test.cljs diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 5c09d951ff..71c7f32405 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -107,6 +107,58 @@ :token-sets token-sets :token-themes token-themes})) +(defn sort-linked-libraries + "Given the linked libraries of a file, each with a `:connected-to` + collection of the ids of the files that use it, return the libraries + ordered as a tree: each library is followed by the libraries nested + under it, marked with `:nested?` and `:parent-name`. + + Direct libraries (`:is-indirect` false) and libraries not used by + any other linked library are roots. Every library is returned + exactly once, at any depth and even when the dependencies form a + cycle." + [libraries] + (let [by-id (d/index-by :id libraries) + sort-by-name (partial sort-by (comp str/lower :name)) + + parents + (d/update-vals by-id + (fn [{:keys [id connected-to]}] + (into #{} + (filter #(and (not= % id) + (contains? by-id %))) + connected-to))) + + root? + (fn [{:keys [id is-indirect]}] + (or (false? is-indirect) + (empty? (get parents id)))) + + children + (fn [parent-id] + (->> libraries + (remove root?) + (filter #(contains? (get parents (:id %)) parent-id)) + (sort-by-name))) + + visit + (fn visit [[result visited :as acc] library parent] + (if (contains? visited (:id library)) + acc + (reduce #(visit %1 %2 library) + [(conj result (assoc library + :nested? (some? parent) + :parent-name (:name parent))) + (conj visited (:id library))] + (children (:id library)))))] + + ;; The second pass picks up libraries only reachable through a + ;; cycle, which have no root to hang from. + (as-> [[] #{}] $ + (reduce #(visit %1 %2 nil) $ (sort-by-name (filter root? libraries))) + (reduce #(visit %1 %2 nil) $ (sort-by-name libraries)) + (first $)))) + (defn- describe-library [components-count graphics-count colors-count typography-count] (let [all-zero? (and (zero? components-count) @@ -666,11 +718,6 @@ dependencies (mf/with-memo [shared-libraries] (into {} (map (juxt :id :library-file-ids) (vals shared-libraries)))) - library-names (mf/with-memo [shared-libraries] - (into {} (map (fn [{:keys [id name]}] - [id name]) - (vals shared-libraries)))) - find-connected-to (mf/use-fn (mf/deps dependencies) @@ -679,36 +726,10 @@ (keep (fn [[k v]] (when (contains? v library-id) k)))))) linked-libraries - (mf/with-memo [linked-libraries find-connected-to library-names] - (let [libs - (->> (vals linked-libraries) - (map #(assoc % :connected-to (find-connected-to (:id %))))) - - linked-ids - (into #{} (map :id) libs) - - sort-by-name - (partial sort-by (comp str/lower :name)) - - parent-id - (fn [{:keys [connected-to]}] - (first (filter linked-ids connected-to))) - - nested-by-parent - (->> libs - (filter parent-id) - (map #(assoc % - :nested? true - :parent-name (library-names (parent-id %)))) - (group-by parent-id) - (d/mapm (fn [_ v] (sort-by-name v))))] - - (->> libs - (remove parent-id) - (map #(assoc % :nested? false)) - (sort-by-name) - (mapcat (fn [{:keys [id] :as library}] - (cons library (get nested-by-parent id []))))))) + (mf/with-memo [linked-libraries find-connected-to] + (->> (vals linked-libraries) + (map #(assoc % :connected-to (find-connected-to (:id %)))) + (sort-linked-libraries))) linked-libraries-ids (mf/with-memo [linked-libraries] diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 493dddeb42..6a897ee8a1 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -120,6 +120,7 @@ [frontend-tests.ui.shortcuts-labels-test] [frontend-tests.ui.sidebar-scroll-test] [frontend-tests.ui.stroke-menu-test] + [frontend-tests.ui.workspace-libraries-test] [frontend-tests.util-clipboard-test] [frontend-tests.util-object-test] [frontend-tests.util-queue-test] @@ -260,6 +261,7 @@ 'frontend-tests.ui.shortcuts-labels-test 'frontend-tests.ui.sidebar-scroll-test 'frontend-tests.ui.stroke-menu-test + 'frontend-tests.ui.workspace-libraries-test 'frontend-tests.util-clipboard-test 'frontend-tests.util-object-test 'frontend-tests.util-queue-test diff --git a/frontend/test/frontend_tests/ui/workspace_libraries_test.cljs b/frontend/test/frontend_tests/ui/workspace_libraries_test.cljs new file mode 100644 index 0000000000..1ed82c488f --- /dev/null +++ b/frontend/test/frontend_tests/ui/workspace_libraries_test.cljs @@ -0,0 +1,101 @@ +;; 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 SUBSIDIARY SL + +(ns frontend-tests.ui.workspace-libraries-test + (:require + [app.main.ui.workspace.libraries :as wl] + [cljs.test :as t :include-macros true])) + +(defn- lib + "A linked library; `connected-to` lists the ids of the files using it." + [id indirect? connected-to] + {:id id :name (name id) :is-indirect indirect? :connected-to connected-to}) + +(defn- summarize + [libraries] + (mapv (juxt :id :nested? :parent-name) libraries)) + +(t/deftest sort-linked-libraries-cycle-with-direct-root + ;; file -> App, Foundations; App -> Foundations; + ;; Foundations <-> Comments (cycle, as left by an import) + (let [libraries [(lib :foundations false [:file :app :comments]) + (lib :app false [:file]) + (lib :comments true [:foundations])] + result (wl/sort-linked-libraries libraries)] + (t/is (= [[:app false nil] + [:foundations false nil] + [:comments true "foundations"]] + (summarize result))) + (t/is (= (count libraries) (count result))))) + +(t/deftest sort-linked-libraries-cycle-without-root + ;; The direct flag is still unknown (nil) while the shared files load, + ;; so both libraries are only reachable through the cycle. `:app` is + ;; not a linked library, so it is ignored as a parent. + (let [libraries [(lib :foundations nil [:app :comments]) + (lib :comments nil [:foundations])] + result (wl/sort-linked-libraries libraries)] + (t/is (= [[:comments false nil] + [:foundations true "comments"]] + (summarize result))) + (t/is (= (count libraries) (count result))))) + +(t/deftest sort-linked-libraries-cycle-of-three-without-root + ;; a -> b -> c -> a, all indirect: no root, nothing is lost + (let [libraries [(lib :c true [:b]) + (lib :b true [:a]) + (lib :a true [:c])] + result (wl/sort-linked-libraries libraries)] + (t/is (= [[:a false nil] + [:b true "a"] + [:c true "b"]] + (summarize result))) + (t/is (= (count libraries) (count result))))) + +(t/deftest sort-linked-libraries-deep-chain + ;; file -> a -> b -> c + (let [libraries [(lib :c true [:b]) + (lib :b true [:a]) + (lib :a false [:file])]] + (t/is (= [[:a false nil] + [:b true "a"] + [:c true "b"]] + (summarize (wl/sort-linked-libraries libraries)))))) + +(t/deftest sort-linked-libraries-diamond + ;; a -> b, c; b -> d; c -> d: d is listed once, under its first parent + (let [libraries [(lib :d true [:b :c]) + (lib :c true [:a]) + (lib :b true [:a]) + (lib :a false [:file])] + result (wl/sort-linked-libraries libraries)] + (t/is (= [[:a false nil] + [:b true "a"] + [:d true "b"] + [:c true "a"]] + (summarize result))) + (t/is (= (count libraries) (count result))))) + +(t/deftest sort-linked-libraries-self-dependency + (let [libraries [(lib :a true [:a])]] + (t/is (= [[:a false nil]] + (summarize (wl/sort-linked-libraries libraries)))))) + +(t/deftest sort-linked-libraries-parent-not-linked + ;; The only user of each library is not a linked library, so both are roots + (let [libraries [(lib :b true [:other]) + (lib :a true [:file])]] + (t/is (= [[:a false nil] + [:b false nil]] + (summarize (wl/sort-linked-libraries libraries)))))) + +(t/deftest sort-linked-libraries-without-dependencies + ;; Before get-team-shared-files answers, all libraries are flat + (let [libraries [(lib :b true nil) + (lib :a false nil)]] + (t/is (= [[:a false nil] + [:b false nil]] + (summarize (wl/sort-linked-libraries libraries)))))) From 31a079a7a2c733c9162c7421aea56b53479968a4 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 8 Oct 2026 09:55:18 +0200 Subject: [PATCH 078/145] :bug: Allow creating tokens when the file is its own tokens source (#12182) Setting the local file back as the tokens source, or unlinking the library that was the source, stored the file's own id in :tokens-source. ensure-tokens-lib only created a tokens-lib when :tokens-source was nil, so a file without a tokens-lib never got one, and every token change was dropped without any error. Now ensure-tokens-lib also creates the tokens-lib when the source is the file itself, which fixes files already in that state, and set-tokens-source removes the attribute instead of storing the file's own id. Closes #12180 AI-assisted-by: claude-opus-5-5 --- common/src/app/common/files/tokens.cljc | 12 +++++++++--- .../test/common_tests/files/tokens_test.cljc | 19 ++++++++++++++++++- .../test/common_tests/logic/token_test.cljc | 4 ++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/common/src/app/common/files/tokens.cljc b/common/src/app/common/files/tokens.cljc index a9506f938e..34c5b760d6 100644 --- a/common/src/app/common/files/tokens.cljc +++ b/common/src/app/common/files/tokens.cljc @@ -460,10 +460,13 @@ :active-set-ids active-set-ids))) (defn ensure-tokens-lib - "Ensure file-data has a :tokens-lib or :tokens-source, and also a :tokens-status, creating them if necessary." + "Ensure file-data has a :tokens-lib or an external :tokens-source, and also a :tokens-status, + creating them if necessary." [file-data] (cond-> file-data - (nil? (:tokens-source file-data)) + (let [tokens-source (:tokens-source file-data)] + (or (nil? tokens-source) + (= tokens-source (:id file-data)))) (update :tokens-lib #(or % (ctob/make-tokens-lib))) :always @@ -488,9 +491,12 @@ (or (:tokens-source file-data) (:id file-data))) (defn set-tokens-source + "Set the tokens source of the file. Setting it to nil or to the file itself + removes the attribute, so the file uses its own tokens-lib." [file-data tokens-source] (assert (or (nil? tokens-source) (uuid? tokens-source)) "expected nil or valid uuid") - (if (nil? tokens-source) + (if (or (nil? tokens-source) + (= tokens-source (:id file-data))) (dissoc file-data :tokens-source) (assoc file-data :tokens-source tokens-source))) diff --git a/common/test/common_tests/files/tokens_test.cljc b/common/test/common_tests/files/tokens_test.cljc index 93b87a12e1..76da45bd4e 100644 --- a/common/test/common_tests/files/tokens_test.cljc +++ b/common/test/common_tests/files/tokens_test.cljc @@ -254,6 +254,14 @@ (t/is (not (contains? file-data' :tokens-lib))) (t/is (= source-id (:tokens-source file-data'))) (t/is (contains? file-data' :tokens-status)) + (t/is (ctos/tokens-status? (:tokens-status file-data'))))) + + (t/testing "ensure-tokens-lib should add a tokens-lib if the tokens-source is the file itself" + (let [file (thf/sample-file :file1) + file-data (-> (ctf/file-data file) + (assoc :tokens-source (:id file))) + file-data' (cfo/ensure-tokens-lib file-data)] + (t/is (ctob/tokens-lib? (:tokens-lib file-data'))) (t/is (ctos/tokens-status? (:tokens-status file-data')))))) (t/deftest test-get-tokens-lib @@ -293,7 +301,16 @@ file (thf/sample-file :file1) file-data (:data file) file-data' (cfo/set-tokens-source file-data source-id)] - (t/is (= source-id (:tokens-source file-data')))))) + (t/is (= source-id (:tokens-source file-data'))))) + + (t/testing "removes tokens-source when set to the file itself" + (let [source-id (thi/new-id! :tokens-source) + file (thf/sample-file :file1) + file-data (-> (ctf/file-data file) + (assoc :tokens-source source-id)) + file-data' (cfo/set-tokens-source file-data (:id file))] + (t/is (not (contains? file-data' :tokens-source))) + (t/is (= (:id file) (cfo/get-effective-tokens-source file-data')))))) (t/deftest test-effective-tokens-source? (t/testing "no tokens, no tokens-source, ask for file id -> true" diff --git a/common/test/common_tests/logic/token_test.cljc b/common/test/common_tests/logic/token_test.cljc index 4ff3d97a10..20ab2e59fb 100644 --- a/common/test/common_tests/logic/token_test.cljc +++ b/common/test/common_tests/logic/token_test.cljc @@ -146,7 +146,7 @@ (t/is (= (ctos/get-active-set-ids file-tokens-status) (ctos/get-active-set-ids undo-tokens-status))))) - (t/testing "if the library is the same file, the source is set to self and the status is the same one" + (t/testing "if the library is the same file, the source is removed and the status is the same one" (let [set-id (uuid/next) file (tht/sample-file-with-tokens :file-id :file1 @@ -166,7 +166,7 @@ undo-tokens-status (tht/get-tokens-status undo)] ;; Redo: source and status copied from library, replacing the previous status - (t/is (= (:id file) redo-tokens-source)) + (t/is (nil? redo-tokens-source)) (t/is (= (ctos/get-active-theme-ids file-tokens-status) (ctos/get-active-theme-ids redo-tokens-status))) (t/is (= (ctos/get-active-set-ids file-tokens-status) From df0402b626e22f204b10764c971b6e4571931cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?andr=C3=A9s=20gonz=C3=A1lez?= Date: Thu, 8 Oct 2026 10:59:36 +0200 Subject: [PATCH 079/145] :lipstick: Align the Help Center footer with penpot.app (#12179) The footer had a hardcoded 2025 copyright and links that no longer match penpot.app. The year is now set at build time. Sections, links and social icons follow the current penpot.app footer, and the grid wraps into two rows to fit the new sections. Closes #12132 AI-assisted-by: claude-opus-5-5 --- docs/_includes/layouts/base.njk | 70 +++++++++++++++++++++++-------- docs/css/index.css | 21 +++++++++- docs/eleventy.config.mjs | 3 ++ docs/img/bluesky.svg | 1 + docs/img/digital-public-good.png | Bin 0 -> 5645 bytes docs/img/peertube.svg | 1 + 6 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 docs/img/bluesky.svg create mode 100644 docs/img/digital-public-good.png create mode 100644 docs/img/peertube.svg diff --git a/docs/_includes/layouts/base.njk b/docs/_includes/layouts/base.njk index 97fc32e20b..4dd96f547c 100644 --- a/docs/_includes/layouts/base.njk +++ b/docs/_includes/layouts/base.njk @@ -89,39 +89,72 @@ + + +