From 29d23577d2bd6a361132dd353316cff947fed0e3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 15 May 2025 11:39:34 +0200 Subject: [PATCH] :tada: Add .penpot (binfile-v3) support for library --- CHANGES.md | 80 +++- backend/src/app/binfile/v3.clj | 55 +-- backend/test/backend_tests/rpc_file_test.clj | 1 + common/src/app/common/files/builder.cljc | 437 ++++++++++-------- common/src/app/common/files/changes.cljc | 4 +- common/src/app/common/media.cljc | 52 +-- common/src/app/common/types/color.cljc | 13 +- common/src/app/common/types/file.cljc | 11 +- frontend/src/app/main/data/exports/files.cljs | 60 ++- .../app/main/data/workspace/libraries.cljs | 2 +- .../src/app/main/data/workspace/media.cljs | 4 +- .../frontend_tests/util_snap_data_test.cljs | 351 +++++++------- library/package.json | 1 - library/playground/sample.jpg | Bin 0 -> 21663 bytes library/playground/sample1.js | 102 ++-- library/shadow-cljs.edn | 4 +- library/src/lib/builder.cljs | 380 ++++++++------- library/src/lib/export.cljs | 93 ++-- library/yarn.lock | 20 +- package.json | 7 +- 20 files changed, 926 insertions(+), 751 deletions(-) create mode 100644 library/playground/sample.jpg diff --git a/CHANGES.md b/CHANGES.md index 263f75f922..c091a73daa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,33 +8,65 @@ **Breaking changes on penpot library:** +The library entrypoint API object has been changed. From now you start creating a new +build context, from where you can add multiple files and attach media. This change add the +ability to build more than one file at same time and export them in an unique .penpot +file. + +```js +const context = penpot.createBuildContext() + +context.addFile({name:"aa"}) +context.addPage({name:"aa"}) +context.closePage() +context.closeFile() + +;; barray is instance of Uint8Array +const barray = penpot.exportAsBytes(context); +``` + +The previous `file.export()` method has been removed and several alternatives are +added as first level functions on penpot library API entrypoint: + +- `exportAsBytes(BuildContext context) -> Promise` +- `exportAsBlob(BuildContext context) -> Promise` +- `exportStream(BuildContext context, WritableStream stream) -> Promise` + +The stream variant allows writting data as it is generated to the stream, without the need +to store the generated output entirelly in the memory. + +There are also relevant semantic changes in how components should be created: this +refactor removes all notions of the old components (v1). Since v2, the shapes that are +part of a component live on a page. So, from now on, to create a component, you should +first create a frame, then add shapes and/or groups to that frame, and then create a +component by declaring that frame as the component root. + +A non exhaustive list of changes: + - Change the signature of the `addPage` method: it now accepts an object (as a single argument) where you can pass `id`, `name`, and `background` props (instead of the previous positional arguments) -- Rename the `file.createRect` method to `file.addRect` -- Rename the `file.createCircle` method to `file.addCircle` -- Rename the `file.createPath` method to `file.addPath` -- Rename the `file.createText` method to `file.addText` -- Rename `file.startComponent` to `file.addComponent` (to preserve the naming style) -- Rename `file.createComponentInstance` to `file.addComponentInstance` (to preserve the naming style) -- Rename `file.lookupShape` to `file.getShape` -- Rename `file.asMap` to `file.toMap` -- Remove `file.updateLibraryColor` (use `file.addLibraryColor` if you just need to replace a color) -- Remove `file.deleteLibraryColor` (this library is intended to build files) -- Remove `file.updateLibraryTypography` (use `file.addLibraryTypography` if you just need to replace a typography) -- Remove `file.deleteLibraryTypography` (this library is intended to build files) -- Remove `file.add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components) -- Remove `file.deleteObject` (this library is intended to build files) -- Remove `file.updateObject` (this library is intended to build files) -- Remove `file.finishComponent` (it is no longer necessary; see below for more details on component creation changes) -- Change the `file.getCurrentPageId` function to a read-only `file.currentPageId` property -- Add `file.currentFrameId` read-only property -- Add `file.lastId` read-only property +- Rename the `createRect` method to `addRect` +- Rename the `createCircle` method to `addCircle` +- Rename the `createPath` method to `addPath` +- Rename the `createText` method to `addText` +- Rename the `addArtboard` method to `addBoard` +- Rename `startComponent` to `addComponent` (to preserve the naming style) +- Rename `createComponentInstance` to `addComponentInstance` (to preserve the naming style) +- Remove `lookupShape` +- Remove `asMap` +- Remove `updateLibraryColor` (use `addLibraryColor` if you just need to replace a color) +- Remove `deleteLibraryColor` (this library is intended to build files) +- Remove `updateLibraryTypography` (use `addLibraryTypography` if you just need to replace a typography) +- Remove `deleteLibraryTypography` (this library is intended to build files) +- Remove `add/update/deleteLibraryMedia` (they are no longer supported by Penpot and have been replaced by components) +- Remove `deleteObject` (this library is intended to build files) +- Remove `updateObject` (this library is intended to build files) +- Remove `finishComponent` (it is no longer necessary; see below for more details on component creation changes) -There are also relevant semantic changes in how components should be created: this refactor removes -all notions of the old components (v1). Since v2, the shapes that are part of a component live on a -page. So, from now on, to create a component, you should first create a frame, then add shapes -and/or groups to that frame, and then create a component by declaring that frame as the component -root. +- Change the `getCurrentPageId` function to a read-only `currentPageId` property +- Add `currentFileId` read-only property +- Add `currentFrameId` read-only property +- Add `lastId` read-only property ### :heart: Community contributions (Thank you!) diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index 961dd2f33f..7659ec5672 100644 --- a/backend/src/app/binfile/v3.clj +++ b/backend/src/app/binfile/v3.clj @@ -18,6 +18,7 @@ [app.common.files.migrations :as-alias fmg] [app.common.json :as json] [app.common.logging :as l] + [app.common.media :as cmedia] [app.common.schema :as sm] [app.common.thumbnails :as cth] [app.common.types.color :as ctcl] @@ -73,7 +74,7 @@ [:size ::sm/int] [:content-type :string] [:bucket [::sm/one-of {:format :string} sto/valid-buckets]] - [:hash :string]]) + [:hash {:optional true} :string]]) (def ^:private schema:file-thumbnail [:map {:title "FileThumbnail"} @@ -88,13 +89,19 @@ ctf/schema:file [:map [:options {:optional true} ctf/schema:options]]]) +;; --- HELPERS + +(defn- default-now + [o] + (or o (dt/now))) + ;; --- ENCODERS (def encode-file (sm/encoder schema:file sm/json-transformer)) (def encode-page - (sm/encoder ::ctp/page sm/json-transformer)) + (sm/encoder ctp/schema:page sm/json-transformer)) (def encode-shape (sm/encoder ::cts/shape sm/json-transformer)) @@ -129,7 +136,7 @@ (sm/decoder schema:manifest sm/json-transformer)) (def decode-media - (sm/decoder ::ctf/media sm/json-transformer)) + (sm/decoder ctf/schema:media sm/json-transformer)) (def decode-component (sm/decoder ::ctc/component sm/json-transformer)) @@ -229,27 +236,13 @@ :always (bfc/clean-file-features)))))) -(defn- resolve-extension - [mtype] - (case mtype - "image/png" ".png" - "image/jpeg" ".jpg" - "image/gif" ".gif" - "image/svg+xml" ".svg" - "image/webp" ".webp" - "font/woff" ".woff" - "font/woff2" ".woff2" - "font/ttf" ".ttf" - "font/otf" ".otf" - "application/octet-stream" ".bin")) - (defn- export-storage-objects [{:keys [::output] :as cfg}] (let [storage (sto/resolve cfg)] (doseq [id (-> bfc/*state* deref :storage-objects not-empty)] (let [sobject (sto/get-object storage id) smeta (meta sobject) - ext (resolve-extension (:content-type smeta)) + ext (cmedia/mtype->extension (:content-type smeta)) path (str "objects/" id ".json") params (-> (meta sobject) (assoc :id (:id sobject)) @@ -574,7 +567,14 @@ (let [object (->> (read-entry input entry) (decode-media) (validate-media)) - object (assoc object :file-id file-id)] + object (-> object + (assoc :file-id file-id) + (update :created-at default-now) + (update :modified-at default-now) + ;; FIXME: this is set default to true for + ;; setting a value, this prop is no longer + ;; relevant; + (assoc :is-local true))] (if (= id (:id object)) (conj result object) result))) @@ -800,7 +800,7 @@ :expected-id (str id) :found-id (str (:id object)))) - (let [ext (resolve-extension (:content-type object)) + (let [ext (cmedia/mtype->extension (:content-type object)) path (str "objects/" id ext) content (->> path (get-zip-entry input) @@ -814,13 +814,14 @@ :expected-size (:size object) :found-size (sto/get-size content))) - (when (not= (:hash object) (sto/get-hash content)) - (ex/raise :type :validation - :code :inconsistent-penpot-file - :hint "found corrupted storage object: hash does not match" - :path path - :expected-hash (:hash object) - :found-hash (sto/get-hash content))) + (when-let [hash (get object :hash)] + (when (not= hash (sto/get-hash content)) + (ex/raise :type :validation + :code :inconsistent-penpot-file + :hint "found corrupted storage object: hash does not match" + :path path + :expected-hash (:hash object) + :found-hash (sto/get-hash content)))) (let [params (-> object (dissoc :id :size) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index da754ef966..951ee96a83 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -1712,6 +1712,7 @@ [{:fill-image {:id (:id fmedia) :name "test" + :mtype "image/jpeg" :width 200 :height 200}}]] diff --git a/common/src/app/common/files/builder.cljc b/common/src/app/common/files/builder.cljc index c62b098dda..c51dccde9a 100644 --- a/common/src/app/common/files/builder.cljc +++ b/common/src/app/common/files/builder.cljc @@ -13,17 +13,12 @@ [app.common.features :as cfeat] [app.common.files.changes :as ch] [app.common.files.migrations :as fmig] - [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.schema :as sm] [app.common.svg :as csvg] [app.common.types.color :as types.color] - [app.common.types.component :as types.component] - [app.common.types.components-list :as types.components-list] - [app.common.types.container :as types.container] [app.common.types.file :as types.file] [app.common.types.page :as types.page] - [app.common.types.pages-list :as types.pages-list] [app.common.types.shape :as types.shape] [app.common.types.typography :as types.typography] [app.common.uuid :as uuid] @@ -37,41 +32,36 @@ (def ^:private conjv (fnil conj [])) (def ^:private conjs (fnil conj #{})) -(defn default-uuid +(defn- default-uuid [v] (or v (uuid/next))) (defn- track-used-name - [file name] - (let [container-id (::current-page-id file)] - (update-in file [::unames container-id] conjs name))) + [state name] + (let [container-id (::current-page-id state)] + (update-in state [::unames container-id] conjs name))) (defn- commit-change - [file change & {:keys [add-container] - :or {add-container false}}] + [state change & {:keys [add-container]}] + (let [file-id (get state ::current-file-id)] + (assert (uuid? file-id) "no current file id") - (let [change (cond-> change - add-container - (assoc :page-id (::current-page-id file) - :frame-id (::current-frame-id file)))] - (-> file - (update ::changes conjv change) - (update :data ch/process-changes [change] false)))) - -(defn- lookup-objects - [file] - (dm/get-in file [:data :pages-index (::current-page-id file) :objects])) + (let [change (cond-> change + add-container + (assoc :page-id (::current-page-id state) + :frame-id (::current-frame-id state)))] + (update-in state [::files file-id :data] ch/process-changes [change] false)))) (defn- commit-shape - [file shape] + [state shape] (let [parent-id - (-> file ::parent-stack peek) + (-> state ::parent-stack peek) frame-id - (::current-frame-id file) + (get state ::current-frame-id) page-id - (::current-page-id file) + (get state ::current-page-id) change {:type :add-obj @@ -82,39 +72,31 @@ :frame-id frame-id :page-id page-id}] - (-> file + (-> state (commit-change change) (track-used-name (:name shape))))) -(defn- generate-name - [type data] - (if (= type :svg-raw) - (let [tag (dm/get-in data [:content :tag])] - (str "svg-" (cond (string? tag) tag - (keyword? tag) (d/name tag) - (nil? tag) "node" - :else (str tag)))) - (str/capital (d/name type)))) - (defn- unique-name - [name file] - (let [container-id (::current-page-id file) - unames (dm/get-in file [:unames container-id])] + [name state] + (let [container-id (::current-page-id state) + unames (dm/get-in state [:unames container-id])] (d/unique-name name (or unames #{})))) (defn- clear-names [file] (dissoc file ::unames)) -(defn- assign-name +(defn- assign-shape-name "Given a tag returns its layer name" - [data file type] - - (cond-> data - (nil? (:name data)) - (assoc :name (generate-name type data)) + [shape state] + (cond-> shape + (nil? (:name shape)) + (assoc :name (let [type (get shape :type)] + (case type + :frame "Board" + (str/capital (d/name type))))) :always - (update :name unique-name file))) + (update :name unique-name state))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SCHEMAS @@ -135,16 +117,25 @@ (def decode-library-typography (sm/decode-fn types.typography/schema:typography sm/json-transformer)) -(def decode-component - (sm/decode-fn types.component/schema:component sm/json-transformer)) +(def schema:add-component + [:map + [:component-id ::sm/uuid] + [:file-id {:optional true} ::sm/uuid] + [:name {:optional true} ::sm/text] + [:path {:optional true} ::sm/text]]) + +(def ^:private check-add-component + (sm/check-fn schema:add-component)) + +(def decode-add-component + (sm/decode-fn schema:add-component sm/json-transformer)) (def schema:add-component-instance [:map [:component-id ::sm/uuid] - [:x ::sm/safe-number] - [:y ::sm/safe-number]]) + [:file-id {:optional true} ::sm/uuid]]) -(def check-add-component-instance +(def ^:private check-add-component-instance (sm/check-fn schema:add-component-instance)) (def decode-add-component-instance @@ -158,37 +149,76 @@ (def decode-add-bool (sm/decode-fn schema:add-bool sm/json-transformer)) -(def check-add-bool +(def ^:private check-add-bool (sm/check-fn schema:add-bool)) +(def schema:add-file-media + [:map + [:id {:optional true} ::sm/uuid] + [:name ::sm/text] + [:width ::sm/int] + [:height ::sm/int]]) + +(def decode-add-file-media + (sm/decode-fn schema:add-file-media sm/json-transformer)) + +(def check-add-file-media + (sm/check-fn schema:add-file-media)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PUBLIC API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn lookup-shape [file shape-id] - (-> (lookup-objects file) - (get shape-id))) +(defn create-state + [] + {}) (defn get-current-page - [file] - (let [page-id (::current-page-id file)] - (dm/get-in file [:data :pages-index page-id]))) + [state] + (let [file-id (get state ::current-file-id) + page-id (get state ::current-page-id)] -(defn create-file - [params] + (assert (uuid? file-id) "expected current-file-id to be assigned") + (assert (uuid? page-id) "expected current-page-id to be assigned") + (dm/get-in state [::files file-id :data :pages-index page-id]))) + +(defn get-current-objects + [state] + (-> (get-current-page state) + (get :objects))) + +(defn get-shape + [state shape-id] + (-> (get-current-objects state) + (get shape-id))) + +(defn add-file + [state params] (let [params (-> params (assoc :features cfeat/default-features) - (assoc :migrations fmig/available-migrations))] - (types.file/make-file params :create-page false))) + (assoc :migrations fmig/available-migrations)) + file (types.file/make-file params :create-page false)] + (-> state + (update ::files assoc (:id file) file) + (assoc ::current-file-id (:id file))))) + +(declare close-page) + +(defn close-file + [state] + (let [state (-> state + (close-page) + (dissoc ::current-file-id))] + state)) (defn add-page - [file params] + [state params] (let [page (-> (types.page/make-empty-page params) (types.page/check-page)) change {:type :add-page :page page}] - (-> file + (-> state (commit-change change) ;; Current page being edited @@ -203,96 +233,96 @@ ;; Last object id added (assoc ::last-id nil)))) -(defn close-page [file] - (-> file +(defn close-page [state] + (-> state (dissoc ::current-page-id) (dissoc ::parent-stack) (dissoc ::last-id) (clear-names))) -(defn add-artboard - [file data] +(defn add-board + [state params] (let [{:keys [id] :as shape} - (-> data + (-> params (update :id default-uuid) (assoc :type :frame) - (assign-name file :frame) + (assign-shape-name state) (types.shape/setup-shape) (types.shape/check-shape))] - (-> file + (-> state (commit-shape shape) (update ::parent-stack conjv id) (assoc ::current-frame-id id) (assoc ::last-id id)))) -(defn close-artboard - [file] - (let [parent-id (-> file ::parent-stack peek) - parent (lookup-shape file parent-id)] - (-> file +(defn close-board + [state] + (let [parent-id (-> state ::parent-stack peek) + parent (get-shape state parent-id)] + (-> state (assoc ::current-frame-id (or (:frame-id parent) root-id)) (update ::parent-stack pop)))) (defn add-group - [file params] + [state params] (let [{:keys [id] :as shape} (-> params (update :id default-uuid) (assoc :type :group) - (assign-name file :group) + (assign-shape-name state) (types.shape/setup-shape) (types.shape/check-shape))] - (-> file + (-> state (commit-shape shape) (assoc ::last-id id) (update ::parent-stack conjv id)))) (defn close-group - [file] - (let [group-id (-> file :parent-stack peek) - group (lookup-shape file group-id) + [state] + (let [group-id (-> state :parent-stack peek) + group (get-shape state group-id) children (->> (get group :shapes) - (into [] (keep (partial lookup-shape file))) + (into [] (keep (partial get-shape state))) (not-empty))] (assert (some? children) "group expect to have at least 1 children") - (let [file (if (:masked-group group) - (let [mask (first children) - change {:type :mod-obj - :id group-id - :operations - [{:type :set :attr :x :val (-> mask :selrect :x) :ignore-touched true} - {:type :set :attr :y :val (-> mask :selrect :y) :ignore-touched true} - {:type :set :attr :width :val (-> mask :selrect :width) :ignore-touched true} - {:type :set :attr :height :val (-> mask :selrect :height) :ignore-touched true} - {:type :set :attr :flip-x :val (-> mask :flip-x) :ignore-touched true} - {:type :set :attr :flip-y :val (-> mask :flip-y) :ignore-touched true} - {:type :set :attr :selrect :val (-> mask :selrect) :ignore-touched true} - {:type :set :attr :points :val (-> mask :points) :ignore-touched true}]}] - (commit-change file change :add-container true)) - (let [group (gsh/update-group-selrect group children) - change {:type :mod-obj - :id group-id - :operations - [{:type :set :attr :selrect :val (:selrect group) :ignore-touched true} - {:type :set :attr :points :val (:points group) :ignore-touched true} - {:type :set :attr :x :val (-> group :selrect :x) :ignore-touched true} - {:type :set :attr :y :val (-> group :selrect :y) :ignore-touched true} - {:type :set :attr :width :val (-> group :selrect :width) :ignore-touched true} - {:type :set :attr :height :val (-> group :selrect :height) :ignore-touched true}]}] + (let [state (if (:masked-group group) + (let [mask (first children) + change {:type :mod-obj + :id group-id + :operations + [{:type :set :attr :x :val (-> mask :selrect :x) :ignore-touched true} + {:type :set :attr :y :val (-> mask :selrect :y) :ignore-touched true} + {:type :set :attr :width :val (-> mask :selrect :width) :ignore-touched true} + {:type :set :attr :height :val (-> mask :selrect :height) :ignore-touched true} + {:type :set :attr :flip-x :val (-> mask :flip-x) :ignore-touched true} + {:type :set :attr :flip-y :val (-> mask :flip-y) :ignore-touched true} + {:type :set :attr :selrect :val (-> mask :selrect) :ignore-touched true} + {:type :set :attr :points :val (-> mask :points) :ignore-touched true}]}] + (commit-change state change :add-container true)) + (let [group (gsh/update-group-selrect group children) + change {:type :mod-obj + :id group-id + :operations + [{:type :set :attr :selrect :val (:selrect group) :ignore-touched true} + {:type :set :attr :points :val (:points group) :ignore-touched true} + {:type :set :attr :x :val (-> group :selrect :x) :ignore-touched true} + {:type :set :attr :y :val (-> group :selrect :y) :ignore-touched true} + {:type :set :attr :width :val (-> group :selrect :width) :ignore-touched true} + {:type :set :attr :height :val (-> group :selrect :height) :ignore-touched true}]}] - (commit-change file change :add-container true)))] - (update file ::parent-stack pop)))) + (commit-change state change :add-container true)))] + (update state ::parent-stack pop)))) (defn add-bool - [file params] + [state params] (let [{:keys [group-id type]} (check-add-bool params) group - (lookup-shape file group-id) + (get-shape state group-id) children (->> (get group :shapes) @@ -300,7 +330,7 @@ (assert (some? children) "expect group to have at least 1 element") - (let [objects (lookup-objects file) + (let [objects (get-current-objects state) bool (-> group (assoc :type :bool) (gsh/update-bool objects)) @@ -317,101 +347,102 @@ {:type :set :attr :width :val (-> bool :selrect :width) :ignore-touched true} {:type :set :attr :height :val (-> bool :selrect :height) :ignore-touched true}]}] - (-> file + (-> state (commit-change change :add-container true) (assoc ::last-id group-id))))) (defn add-shape - [file params] + [state params] (let [obj (-> params (d/update-when :svg-attrs csvg/attrs->props) (types.shape/setup-shape) - (assign-name file :type))] - (-> file + (assign-shape-name state))] + (-> state (commit-shape obj) (assoc ::last-id (:id obj))))) (defn add-library-color - [file color] + [state color] (let [color (-> color + (update :opacity d/nilv 1) (update :id default-uuid) (types.color/check-library-color color)) + change {:type :add-color :color color}] - (-> file + + (-> state (commit-change change) (assoc ::last-id (:id color))))) (defn add-library-typography - [file typography] + [state typography] (let [typography (-> typography (update :id default-uuid) (d/without-nils)) change {:type :add-typography :id (:id typography) :typography typography}] - (-> file + (-> state (commit-change change) (assoc ::last-id (:id typography))))) (defn add-component - [file params] - (let [change1 {:type :add-component - :id (or (:id params) (uuid/next)) - :name (:name params) - :path (:path params) - :main-instance-id (:main-instance-id params) - :main-instance-page (:main-instance-page params)} + [state params] + (let [{:keys [component-id file-id name path]} + (check-add-component params) - comp-id (get change1 :id) - - change2 {:type :mod-obj - :id (:main-instance-id params) - :operations - [{:type :set :attr :component-root :val true} - {:type :set :attr :component-id :val comp-id} - {:type :set :attr :component-file :val (:id file)}]}] - (-> file - (commit-change change1) - (commit-change change2) - (assoc ::last-id comp-id) - (assoc ::current-frame-id comp-id)))) - -(defn add-component-instance - [{:keys [id data] :as file} params] - - (let [{:keys [component-id x y]} - (check-add-component-instance params) - - component - (types.components-list/get-component data component-id) + frame-id + (get state ::current-frame-id) page-id - (get file ::current-page-id)] + (get state ::current-page-id) - (assert (uuid? page-id) "page-id is expected to be set") - (assert (uuid? component) "component is expected to exist") + component-id + (or component-id (uuid/next)) - ;; FIXME: this should be on files and not in pages-list - (let [page (types.pages-list/get-page (:data file) page-id) - pos (gpt/point x y) + change1 + (d/without-nils + {:type :add-component + :id component-id + :name (or name "anonmous") + :path path + :main-instance-id frame-id + :main-instance-page page-id}) - [shape shapes] - (types.container/make-component-instance page component id pos) + change2 + {:type :mod-obj + :id frame-id + :operations + [{:type :set :attr :component-root :val true} + {:type :set :attr :component-id :val component-id} + {:type :set :attr :component-file :val file-id}]}] - file - (reduce #(commit-change %1 - {:type :add-obj - :id (:id %2) - :page-id (:id page) - :parent-id (:parent-id %2) - :frame-id (:frame-id %2) - :ignore-touched true - :obj %2}) - file - shapes)] + (-> state + (commit-change change1) + (commit-change change2)))) - (assoc file ::last-id (:id shape))))) +(defn add-component-instance + [state params] + + (let [{:keys [component-id file-id]} + (check-add-component-instance params) + + file-id + (or file-id (get state ::current-file-id)) + + frame-id + (get state ::current-frame-id) + + change + {:type :mod-obj + :id frame-id + :operations + [{:type :set :attr :component-root :val false} + {:type :set :attr :component-id :val component-id} + {:type :set :attr :component-file :val file-id}]}] + + (commit-change state change))) (defn delete-shape [file id] @@ -423,10 +454,12 @@ :id id})) (defn update-shape - [file shape-id f] - (let [page-id (::current-page-id file) - objects (lookup-objects file) + [state shape-id f] + (let [page-id (get state ::current-page-id) + + objects (get-current-objects state) old-shape (get objects shape-id) + new-shape (f old-shape) attrs (d/concat-set (keys old-shape) @@ -440,7 +473,7 @@ changes (conj changes {:type :set :attr attr :val new-val :ignore-touched true}))))] - (-> file + (-> state (commit-change {:type :mod-obj :operations (reduce generate-operation [] attrs) @@ -449,12 +482,12 @@ (assoc ::last-id shape-id)))) (defn add-guide - [file guide] + [state guide] (let [guide (cond-> guide (nil? (:id guide)) (assoc :id (uuid/next))) - page-id (::current-page-id file)] - (-> file + page-id (::current-page-id state)] + (-> state (commit-change {:type :set-guide :page-id page-id @@ -463,24 +496,54 @@ (assoc ::last-id (:id guide))))) (defn delete-guide - [file id] - - (let [page-id (::current-page-id file)] - (commit-change file + [state id] + (let [page-id (::current-page-id state)] + (commit-change state {:type :set-guide :page-id page-id :id id :params nil}))) (defn update-guide - [file guide] - (let [page-id (::current-page-id file)] - (commit-change file + [state guide] + (let [page-id (::current-page-id state)] + (commit-change state {:type :set-guide :page-id page-id :id (:id guide) :params guide}))) -(defn strip-image-extension [filename] - (let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"] - (str/replace filename image-extensions-re ""))) +(defrecord BlobWrapper [mtype size blob]) + +(defn add-file-media + [state params blob] + (assert (instance? BlobWrapper blob) "expect blob to be wrapped") + + (let [media-id + (uuid/next) + + file-id + (get state ::current-file-id) + + {:keys [id width height name]} + (-> params + (update :id default-uuid) + (check-add-file-media params))] + + (-> state + (update ::blobs assoc media-id blob) + (update ::media assoc media-id + {:id media-id + :bucket "file-media-object" + :content-type (get blob :mtype) + :size (get blob :size)}) + (update ::file-media assoc id + {:id id + :name name + :width width + :height height + :file-id file-id + :media-id media-id + :mtype (get blob :mtype)}) + + (assoc ::last-id id)))) diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 23dda4ac17..47409130fb 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -310,12 +310,12 @@ [:add-media [:map {:title "AddMediaChange"} [:type [:= :add-media]] - [:object ::ctf/media-object]]] + [:object ctf/schema:media]]] [:mod-media [:map {:title "ModMediaChange"} [:type [:= :mod-media]] - [:object ::ctf/media-object]]] + [:object ctf/schema:media]]] [:del-media [:map {:title "DelMediaChange"} diff --git a/common/src/app/common/media.cljc b/common/src/app/common/media.cljc index a342a227f0..b6f2311fb5 100644 --- a/common/src/app/common/media.cljc +++ b/common/src/app/common/media.cljc @@ -5,8 +5,8 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.media + "Media assets helpers (images, fonts, etc)" (:require - [clojure.spec.alpha :as s] [cuerdas.core :as str])) ;; We have added ".ttf" as string to solve a problem with chrome input selector @@ -48,38 +48,28 @@ (defn mtype->extension [mtype] ;; https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types (case mtype - "image/apng" ".apng" - "image/avif" ".avif" - "image/gif" ".gif" - "image/jpeg" ".jpg" - "image/png" ".png" - "image/svg+xml" ".svg" - "image/webp" ".webp" - "application/zip" ".zip" - "application/penpot" ".penpot" - "application/pdf" ".pdf" - "text/plain" ".txt" + "image/apng" ".apng" + "image/avif" ".avif" + "image/gif" ".gif" + "image/jpeg" ".jpg" + "image/png" ".png" + "image/svg+xml" ".svg" + "image/webp" ".webp" + "application/zip" ".zip" + "application/penpot" ".penpot" + "application/pdf" ".pdf" + "text/plain" ".txt" + "font/woff" ".woff" + "font/woff2" ".woff2" + "font/ttf" ".ttf" + "font/otf" ".otf" + "application/octet-stream" ".bin" nil)) -(s/def ::id uuid?) -(s/def ::name string?) -(s/def ::width number?) -(s/def ::height number?) -(s/def ::created-at inst?) -(s/def ::modified-at inst?) -(s/def ::mtype string?) -(s/def ::uri string?) - -(s/def ::media-object - (s/keys :req-un [::id - ::name - ::width - ::height - ::mtype - ::created-at - ::modified-at - ::uri])) - +(defn strip-image-extension + [filename] + (let [image-extensions-re #"(\.png)|(\.jpg)|(\.jpeg)|(\.webp)|(\.gif)|(\.svg)$"] + (str/replace filename image-extensions-re ""))) (defn parse-font-weight [variant] diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index eb2018fad7..9fda10d36b 100644 --- a/common/src/app/common/types/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -54,13 +54,13 @@ ::oapi/type "integer" ::oapi/format "int64"}})) -(def schema:image-color +(def schema:image [:map {:title "ImageColor"} - [:name {:optional true} :string] [:width ::sm/int] [:height ::sm/int] - [:mtype {:optional true} [:maybe :string]] + [:mtype ::sm/text] [:id ::sm/uuid] + [:name {:optional true} ::sm/text] [:keep-aspect-ratio {:optional true} :boolean]]) (def gradient-types @@ -93,7 +93,7 @@ [:ref-id {:optional true} ::sm/uuid] [:ref-file {:optional true} ::sm/uuid] [:gradient {:optional true} [:maybe schema:gradient]] - [:image {:optional true} [:maybe schema:image-color]] + [:image {:optional true} [:maybe schema:image]] [:plugin-data {:optional true} ::ctpg/plugin-data]]) (def schema:color @@ -106,7 +106,7 @@ [:opacity {:optional true} [:maybe ::sm/safe-number]] [:color {:optional true} [:maybe schema:rgb-color]] [:gradient {:optional true} [:maybe schema:gradient]] - [:image {:optional true} [:maybe schema:image-color]]] + [:image {:optional true} [:maybe schema:image]]] [::sm/contains-any {:strict true} [:color :gradient :image]]]) ;; Same as color but with :id prop required @@ -115,9 +115,10 @@ (sm/required-keys schema:color-attrs [:id]) [::sm/contains-any {:strict true} [:color :gradient :image]]]) +;; FIXME: revisit if we really need this all registers (sm/register! ::color schema:color) (sm/register! ::gradient schema:gradient) -(sm/register! ::image-color schema:image-color) +(sm/register! ::image-color schema:image) (sm/register! ::recent-color schema:recent-color) (sm/register! ::color-attrs schema:color-attrs) diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index e4ee9ceb9e..ca3a3e94f8 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -38,18 +38,18 @@ (def schema:media "A schema that represents the file media object" - [:map {:title "FileMediaObject"} + [:map {:title "FileMedia"} [:id ::sm/uuid] - [:created-at ::sm/inst] + [:created-at {:optional true} ::sm/inst] [:deleted-at {:optional true} ::sm/inst] [:name :string] [:width ::sm/safe-int] [:height ::sm/safe-int] [:mtype :string] - [:file-id {:optional true} ::sm/uuid] [:media-id ::sm/uuid] + [:file-id {:optional true} ::sm/uuid] [:thumbnail-id {:optional true} ::sm/uuid] - [:is-local :boolean]]) + [:is-local {:optional true} :boolean]]) (def schema:colors [:map-of {:gen/max 5} ::sm/uuid ::ctc/color]) @@ -102,7 +102,6 @@ (sm/register! ::media schema:media) (sm/register! ::colors schema:colors) (sm/register! ::typographies schema:typographies) -(sm/register! ::media-object schema:media) (def check-file (sm/check-fn schema:file :hint "check error on validating file")) @@ -110,7 +109,7 @@ (def check-file-data (sm/check-fn schema:data)) -(def check-media-object +(def check-file-media (sm/check-fn schema:media)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/exports/files.cljs b/frontend/src/app/main/data/exports/files.cljs index fd5bd28350..74001f1f9f 100644 --- a/frontend/src/app/main/data/exports/files.cljs +++ b/frontend/src/app/main/data/exports/files.cljs @@ -34,41 +34,35 @@ (defn export-files [files format] - (dm/assert! - "expected valid files param" - (check-export-files files)) + (assert (contains? valid-formats format) + "expected valid export format") - (dm/assert! - "expected valid format" - (contains? valid-formats format)) + (let [files (check-export-files files)] - (ptk/reify ::export-files - ptk/WatchEvent - (watch [_ state _] - (let [features (get state :features) - team-id (:current-team-id state) - evname (if (= format :legacy-zip) - "export-standard-files" - "export-binary-files")] - - (rx/merge - (rx/of (ptk/event ::ev/event {::ev/name evname - ::ev/origin "dashboard" - :format format - :num-files (count files)})) - (->> (rx/from files) - (rx/mapcat - (fn [file] - (->> (rp/cmd! :has-file-libraries {:file-id (:id file)}) - (rx/map #(assoc file :has-libraries %))))) - (rx/reduce conj []) - (rx/map (fn [files] - (modal/show - {:type ::export-files - :features features - :team-id team-id - :files files - :format format}))))))))) + (ptk/reify ::export-files + ptk/WatchEvent + (watch [_ state _] + (let [team-id (get state :current-team-id) + evname (if (= format :legacy-zip) + "export-standard-files" + "export-binary-files")] + (rx/merge + (rx/of (ptk/event ::ev/event {::ev/name evname + ::ev/origin "dashboard" + :format format + :num-files (count files)})) + (->> (rx/from files) + (rx/mapcat + (fn [file] + (->> (rp/cmd! :has-file-libraries {:file-id (:id file)}) + (rx/map #(assoc file :has-libraries %))))) + (rx/reduce conj []) + (rx/map (fn [files] + (modal/show + {:type ::export-files + :team-id team-id + :files files + :format format})))))))))) ;;;;;;;;;;;;;;;;;;;;;; ;; Team Request diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 67c9d18715..92a5f81a45 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -254,7 +254,7 @@ (defn add-media [media] - (let [media (ctf/check-media-object media)] + (let [media (ctf/check-file-media media)] (ptk/reify ::add-media ev/Event (-data [_] media) diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index 60c4ff25b0..57b3d491e3 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -10,11 +10,11 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] - [app.common.files.builder :as fb] [app.common.files.changes-builder :as pcb] [app.common.files.shapes-builder :as sb] [app.common.logging :as log] [app.common.math :as mth] + [app.common.media :as media] [app.common.schema :as sm] [app.common.types.container :as ctn] [app.common.types.shape :as cts] @@ -137,7 +137,7 @@ (= (.-type blob) "image/svg+xml"))) (prepare-blob [blob] - (let [name (or name (if (dmm/file? blob) (fb/strip-image-extension (.-name blob)) "blob"))] + (let [name (or name (if (dmm/file? blob) (media/strip-image-extension (.-name blob)) "blob"))] {:file-id file-id :name name :is-local local? diff --git a/frontend/test/frontend_tests/util_snap_data_test.cljs b/frontend/test/frontend_tests/util_snap_data_test.cljs index f46fba7d6a..0ad546a672 100644 --- a/frontend/test/frontend_tests/util_snap_data_test.cljs +++ b/frontend/test/frontend_tests/util_snap_data_test.cljs @@ -27,7 +27,8 @@ (t/is (some? data)))) (t/testing "Add empty page (only root-frame)" - (let [page (-> (fb/create-file {:name "Test"}) + (let [page (-> (fb/create-state) + (fb/add-file {:name "Test"}) (fb/add-page {:name "Page 1"}) (fb/get-current-page)) @@ -36,18 +37,19 @@ (t/is (some? data)))) (t/testing "Create simple shape on root" - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-shape - {:type :rect - :x 0 - :y 0 - :width 100 - :height 100})) - page (fb/get-current-page file) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-shape + {:type :rect + :x 0 + :y 0 + :width 100 + :height 100})) + page (fb/get-current-page state) - data (-> (sd/make-snap-data) - (sd/add-page page)) + data (-> (sd/make-snap-data) + (sd/add-page page)) result-x (sd/query data (:id page) uuid/zero :x [0 100])] @@ -66,17 +68,18 @@ (t/is (= (first (nth result-x 2)) 100)))) (t/testing "Add page with single empty frame" - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-artboard - {:x 0 - :y 0 - :width 100 - :height 100}) - (fb/close-artboard)) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-board + {:x 0 + :y 0 + :width 100 + :height 100}) + (fb/close-board)) - frame-id (::fb/last-id file) - page (fb/get-current-page file) + frame-id (::fb/last-id state) + page (fb/get-current-page state) ;; frame-id (::fb/last-id file) data (-> (sd/make-snap-data) @@ -91,26 +94,27 @@ (t/testing "Add page with some shapes inside frames" (with-redefs [uuid/next (get-mocked-uuid)] - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-artboard - {:x 0 - :y 0 - :width 100 - :height 100})) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-board + {:x 0 + :y 0 + :width 100 + :height 100})) - frame-id (::fb/last-id file) + frame-id (::fb/last-id state) - file (-> file - (fb/add-shape - {:type :rect - :x 25 - :y 25 - :width 50 - :height 50}) - (fb/close-artboard)) + state (-> state + (fb/add-shape + {:type :rect + :x 25 + :y 25 + :width 50 + :height 50}) + (fb/close-board)) - page (fb/get-current-page file) + page (fb/get-current-page state) data (-> (sd/make-snap-data) (sd/add-page page)) @@ -123,21 +127,21 @@ (t/is (= (count result-frame-x) 5))))) (t/testing "Add a global guide" - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-guide {:position 50 :axis :x}) - (fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) - (fb/close-artboard)) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-guide {:position 50 :axis :x}) + (fb/add-board {:x 200 :y 200 :width 100 :height 100}) + (fb/close-board)) - frame-id (::fb/last-id file) - page (fb/get-current-page file) + frame-id (::fb/last-id state) + page (fb/get-current-page state) - ;; frame-id (::fb/last-id file) - data (-> (sd/make-snap-data) - (sd/add-page page)) + data (-> (sd/make-snap-data) + (sd/add-page page)) - result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) - result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) + result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) + result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) result-frame-x (sd/query data (:id page) frame-id :x [0 100]) result-frame-y (sd/query data (:id page) frame-id :y [0 100])] @@ -151,17 +155,18 @@ (t/is (= (count result-frame-y) 0)))) (t/testing "Add a frame guide" - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) - (fb/close-artboard)) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-board {:x 200 :y 200 :width 100 :height 100}) + (fb/close-board)) - frame-id (::fb/last-id file) + frame-id (::fb/last-id state) - file (-> file - (fb/add-guide {:position 50 :axis :x :frame-id frame-id})) + state (-> state + (fb/add-guide {:position 50 :axis :x :frame-id frame-id})) - page (fb/get-current-page file) + page (fb/get-current-page state) data (-> (sd/make-snap-data) (sd/add-page page)) @@ -182,26 +187,26 @@ (t/deftest test-update-index (t/testing "Create frame on root and then remove it." - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-artboard - {:x 0 - :y 0 - :width 100 - :height 100}) - (fb/close-artboard)) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-board + {:x 0 + :y 0 + :width 100 + :height 100}) + (fb/close-board)) - shape-id (::fb/last-id file) - page (fb/get-current-page file) + shape-id (::fb/last-id state) + page (fb/get-current-page state) - ;; frame-id (::fb/last-id file) data (-> (sd/make-snap-data) (sd/add-page page)) - file (-> file - (fb/delete-shape shape-id)) + state (-> state + (fb/delete-shape shape-id)) - new-page (fb/get-current-page file) + new-page (fb/get-current-page state) data (sd/update-page data page new-page) result-x (sd/query data (:id page) uuid/zero :x [0 100]) @@ -212,25 +217,26 @@ (t/is (= (count result-y) 0)))) (t/testing "Create simple shape on root. Then remove it" - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-shape - {:type :rect - :x 0 - :y 0 - :width 100 - :height 100})) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-shape + {:type :rect + :x 0 + :y 0 + :width 100 + :height 100})) - shape-id (::fb/last-id file) - page (fb/get-current-page file) + shape-id (::fb/last-id state) + page (fb/get-current-page state) - ;; frame-id (::fb/last-id file) + ;; frame-id (::fb/last-id state) data (-> (sd/make-snap-data) (sd/add-page page)) - file (fb/delete-shape file shape-id) + state (fb/delete-shape state shape-id) - new-page (fb/get-current-page file) + new-page (fb/get-current-page state) data (sd/update-page data page new-page) result-x (sd/query data (:id page) uuid/zero :x [0 100]) @@ -241,30 +247,31 @@ (t/is (= (count result-y) 0)))) (t/testing "Create shape inside frame, then remove it" - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-artboard - {:x 0 - :y 0 - :width 100 - :height 100})) - frame-id (::fb/last-id file) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-board + {:x 0 + :y 0 + :width 100 + :height 100})) + frame-id (::fb/last-id state) - file (fb/add-shape file {:type :rect :x 25 :y 25 :width 50 :height 50}) - shape-id (::fb/last-id file) + state (fb/add-shape state {:type :rect :x 25 :y 25 :width 50 :height 50}) + shape-id (::fb/last-id state) - file (fb/close-artboard file) + state (fb/close-board state) - page (fb/get-current-page file) - data (-> (sd/make-snap-data) - (sd/add-page page)) + page (fb/get-current-page state) + data (-> (sd/make-snap-data) + (sd/add-page page)) - file (fb/delete-shape file shape-id) - new-page (fb/get-current-page file) + state (fb/delete-shape state shape-id) + new-page (fb/get-current-page state) - data (sd/update-page data page new-page) + data (sd/update-page data page new-page) - result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) + result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) result-frame-x (sd/query data (:id page) frame-id :x [0 100])] (t/is (some? data)) @@ -272,26 +279,28 @@ (t/is (= (count result-frame-x) 3)))) (t/testing "Create global guide then remove it" - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-guide {:position 50 :axis :x})) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-guide {:position 50 :axis :x})) - guide-id (::fb/last-id file) + guide-id (::fb/last-id state) - file (-> (fb/add-artboard file {:x 200 :y 200 :width 100 :height 100}) - (fb/close-artboard)) + state (-> (fb/add-board state {:x 200 :y 200 :width 100 :height 100}) + (fb/close-board)) - frame-id (::fb/last-id file) - page (fb/get-current-page file) - data (-> (sd/make-snap-data) (sd/add-page page)) + frame-id (::fb/last-id state) + page (fb/get-current-page state) + data (-> (sd/make-snap-data) + (sd/add-page page)) - new-page (-> (fb/delete-guide file guide-id) + new-page (-> (fb/delete-guide state guide-id) (fb/get-current-page)) - data (sd/update-page data page new-page) + data (sd/update-page data page new-page) - result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) - result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) + result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) + result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) result-frame-x (sd/query data (:id page) frame-id :x [0 100]) result-frame-y (sd/query data (:id page) frame-id :y [0 100])] @@ -305,10 +314,11 @@ (t/is (= (count result-frame-y) 0)))) (t/testing "Create frame guide then remove it" - (let [file (-> (fb/create-file {:name "Test"}) + (let [file (-> (fb/create-state) + (fb/add-file {:name "Test"}) (fb/add-page {:name "Page 1"}) - (fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) - (fb/close-artboard)) + (fb/add-board {:x 200 :y 200 :width 100 :height 100}) + (fb/close-board)) frame-id (::fb/last-id file) file (fb/add-guide file {:position 50 :axis :x :frame-id frame-id}) @@ -336,30 +346,31 @@ (t/is (= (count result-frame-y) 0)))) (t/testing "Update frame coordinates" - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-artboard - {:x 0 - :y 0 - :width 100 - :height 100}) - (fb/close-artboard)) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-board + {:x 0 + :y 0 + :width 100 + :height 100}) + (fb/close-board)) - frame-id (::fb/last-id file) - page (fb/get-current-page file) - data (-> (sd/make-snap-data) (sd/add-page page)) + frame-id (::fb/last-id state) + page (fb/get-current-page state) + data (-> (sd/make-snap-data) + (sd/add-page page)) - file (fb/update-shape file frame-id - (fn [shape] - (-> shape - (dissoc :selrect :points) - (assoc :x 200 :y 200) - (cts/setup-shape)))) + state (fb/update-shape state frame-id + (fn [shape] + (-> shape + (dissoc :selrect :points) + (assoc :x 200 :y 200) + (cts/setup-shape)))) - new-page (fb/get-current-page file) - - data (sd/update-page data page new-page) + new-page (fb/get-current-page state) + data (sd/update-page data page new-page) result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100]) result-frame-x-1 (sd/query data (:id page) frame-id :x [0 100]) @@ -373,28 +384,29 @@ (t/is (= (count result-frame-x-2) 3)))) (t/testing "Update shape coordinates" - (let [file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-shape - {:type :rect - :x 0 - :y 0 - :width 100 - :height 100})) + (let [state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-shape + {:type :rect + :x 0 + :y 0 + :width 100 + :height 100})) - shape-id (::fb/last-id file) - page (fb/get-current-page file) + shape-id (::fb/last-id state) + page (fb/get-current-page state) data (-> (sd/make-snap-data) (sd/add-page page)) - file (fb/update-shape file shape-id - (fn [shape] - (-> shape - (dissoc :selrect :points) - (assoc :x 200 :y 200) - (cts/setup-shape)))) + state (fb/update-shape state shape-id + (fn [shape] + (-> shape + (dissoc :selrect :points) + (assoc :x 200 :y 200) + (cts/setup-shape)))) - new-page (fb/get-current-page file) + new-page (fb/get-current-page state) ;; FIXME: update data (sd/update-page data page new-page) @@ -406,25 +418,26 @@ (t/is (= (count result-zero-x-2) 3)))) (t/testing "Update global guide" - (let [guide {:position 50 :axis :x} - file (-> (fb/create-file {:name "Test"}) - (fb/add-page {:name "Page 1"}) - (fb/add-guide guide)) + (let [guide {:position 50 :axis :x} + state (-> (fb/create-state) + (fb/add-file {:name "Test"}) + (fb/add-page {:name "Page 1"}) + (fb/add-guide guide)) - guide-id (::fb/last-id file) - guide (assoc guide :id guide-id) + guide-id (::fb/last-id state) + guide (assoc guide :id guide-id) - file (-> (fb/add-artboard file {:x 500 :y 500 :width 100 :height 100}) - (fb/close-artboard)) + state (-> (fb/add-board state {:x 500 :y 500 :width 100 :height 100}) + (fb/close-board)) - frame-id (::fb/last-id file) - page (fb/get-current-page file) + frame-id (::fb/last-id state) + page (fb/get-current-page state) data (-> (sd/make-snap-data) (sd/add-page page)) - new-page (-> (fb/update-guide file (assoc guide :position 150)) + new-page (-> (fb/update-guide state (assoc guide :position 150)) (fb/get-current-page)) - data (sd/update-page data page new-page) + data (sd/update-page data page new-page) result-zero-x-1 (sd/query data (:id page) uuid/zero :x [0 100]) result-zero-y-1 (sd/query data (:id page) uuid/zero :y [0 100]) diff --git a/library/package.json b/library/package.json index 21f9041937..f4664da6d1 100644 --- a/library/package.json +++ b/library/package.json @@ -33,7 +33,6 @@ "dependencies": { "@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch", "luxon": "^3.6.1", - "sax": "^1.4.1", "source-map-support": "^0.5.21" } } diff --git a/library/playground/sample.jpg b/library/playground/sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9476988529e0ed8cd924c5d3b0e3231b1731ed82 GIT binary patch literal 21663 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 literal 0 HcmV?d00001 diff --git a/library/playground/sample1.js b/library/playground/sample1.js index f75d737693..db8b51016f 100644 --- a/library/playground/sample1.js +++ b/library/playground/sample1.js @@ -1,47 +1,87 @@ import * as penpot from "../target/library/penpot.js"; -import { writeFile } from 'fs/promises'; +import { writeFile, readFile } from 'fs/promises'; import { createWriteStream } from 'fs'; import { Writable } from "stream"; -console.log(penpot); +// console.log(penpot); (async function() { - const file = penpot.createFile({name: "Test"}); - - file.addPage({name: "Foo Page"}) - const boardId = file.addArtboard({name: "Foo Board"}) - const rectId = file.addRect({name: "Foo Rect", width:100, height: 200}) - - file.addLibraryColor({color: "#fabada", opacity: 0.5}) - - // console.log("created board", boardId); - // console.log("created rect", rectId); - - // const board = file.getShape(boardId); - // console.log("=========== BOARD =============") - // console.dir(board, {depth: 10}); - - // const rect = file.getShape(rectId); - // console.log("=========== RECT =============") - // console.dir(rect, {depth: 10}); + const context = penpot.createBuildContext(); { - let result = await penpot.exportAsBytes(file) + context.addFile({name: "Test File 1"}); + context.addPage({name: "Foo Page"}) + + // Add image media + const buffer = await readFile("./playground/sample.jpg"); + const blob = new Blob([buffer], { type: 'image/jpeg' }); + + const mediaId = context.addFileMedia({ + name: "avatar.jpg", + width: 512, + height: 512 + }, blob); + + // Add image color asset + const assetColorId = context.addLibraryColor({ + name: "Avatar", + opacity: 1, + image: { + ...context.getMediaAsImage(mediaId), + keepAspectRatio: true + } + }); + + const boardId = context.addBoard({ + name: "Foo Board", + x: 0, + y: 0, + width: 500, + height: 300, + }) + + const fill = { + fillColorRefId: assetColorId, + fillColorRefFile: context.currentFileId, + fillImage: { + ...context.getMediaAsImage(mediaId), + keepAspectRatio: true + } + }; + + context.addRect({ + name: "Rect 1", + x: 20, + y: 20, + width:100, + height:200, + fills: [fill] + }); + + context.closeBoard(); + context.closeFile(); + } + + { + let result = await penpot.exportAsBytes(context) await writeFile("sample-sync.zip", result); } - { - // Create a file stream to write the zip to - const output = createWriteStream('sample-stream.zip'); - - // Wrap Node's stream in a WHATWG WritableStream - const writable = Writable.toWeb(output); - - await penpot.exportStream(file, writable); - } + // { + // // Create a file stream to write the zip to + // const output = createWriteStream('sample-stream.zip'); + // // Wrap Node's stream in a WHATWG WritableStream + // const writable = Writable.toWeb(output); + // await penpot.exportStream(context, writable); + // } })().catch((cause) => { - console.log(cause); + console.error(cause); + + const innerCause = cause.cause; + if (innerCause) { + console.error("Inner cause:", innerCause); + } process.exit(-1); }).finally(() => { process.exit(0); diff --git a/library/shadow-cljs.edn b/library/shadow-cljs.edn index 862a53e39f..5eee3a6993 100644 --- a/library/shadow-cljs.edn +++ b/library/shadow-cljs.edn @@ -25,7 +25,7 @@ :modules {:penpot {:exports {BuilderError lib.builder/BuilderError - createFile lib.builder/create-file + createBuildContext lib.builder/create-build-context exportAsBytes lib.export/export-bytes exportAsBlob lib.export/export-blob exportStream lib.export/export-stream @@ -34,7 +34,7 @@ :js-options {:entry-keys ["module" "browser" "main"] :export-conditions ["module" "import", "browser" "require" "default"] - :js-provider :import + ;; :js-provider :import ;; :external-index "target/library/dependencies.js" ;; :external-index-format :esm } diff --git a/library/src/lib/builder.cljs b/library/src/lib/builder.cljs index d39469345a..4cd19dd639 100644 --- a/library/src/lib/builder.cljs +++ b/library/src/lib/builder.cljs @@ -55,201 +55,235 @@ (defn- decode-params [params] (if (obj/plain-object? params) - (json/->js params) + (json/->clj params) params)) -(defn- create-file-api - [file] - (let [state* (volatile! file) - api (obj/reify {:name "File"} - :id - {:get #(dm/str (:id @state*))} +(defn- get-current-page-id + [state] + (dm/str (get state ::fb/current-page-id))) - :currentFrameId - {:get #(dm/str (::fb/current-frame-id @state*))} +(defn- get-last-id + [state] + (dm/str (get state ::fb/last-id))) - :currentPageId - {:get #(dm/str (::fb/current-page-id @state*))} +(defn- create-builder-api + [state] + (obj/reify {:name "File"} + :currentFileId + {:get #(dm/str (get @state ::fb/current-file-id))} - :lastId - {:get #(dm/str (::fb/last-id @state*))} + :currentFrameId + {:get #(dm/str (get @state ::fb/current-frame-id))} - :addPage - (fn [params] - (try - (let [params (-> params - (decode-params) - (fb/decode-page))] - (vswap! state* fb/add-page params) - (dm/str (::fb/current-page-id @state*))) - (catch :default cause - (handle-exception cause)))) + :currentPageId + {:get #(get-current-page-id @state)} - :closePage - (fn [] - (vswap! state* fb/close-page)) + :lastId + {:get #(get-last-id @state)} - :addArtboard - (fn [params] - (try - (let [params (-> params - (json/->clj) - (assoc :type :frame) - (fb/decode-shape))] - (vswap! state* fb/add-artboard params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + :addFile + (fn [params] + (try + (let [params (-> params decode-params fb/decode-file)] + (-> (swap! state fb/add-file params) + (get ::fb/current-file-id))) + (catch :default cause + (handle-exception cause)))) - :closeArtboard - (fn [] - (vswap! state* fb/close-artboard)) + :closeFile + (fn [] + (swap! state fb/close-file) + nil) - :addGroup - (fn [params] - (try - (let [params (-> params - (json/->clj) - (assoc :type :group) - (fb/decode-shape))] - (vswap! state* fb/add-group params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + :addPage + (fn [params] + (try + (let [params (-> (decode-params params) + (fb/decode-page))] - :closeGroup - (fn [] - (vswap! state* fb/close-group)) + (-> (swap! state fb/add-page params) + (get-current-page-id))) - :addBool - (fn [params] - (try - (let [params (-> params - (json/->clj) - (fb/decode-add-bool))] - (vswap! state* fb/add-bool params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + (catch :default cause + (handle-exception cause)))) - :addRect - (fn [params] - (try - (let [params (-> params - (json/->clj) - (assoc :type :rect) - (fb/decode-shape))] - (vswap! state* fb/add-shape params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + :closePage + (fn [] + (swap! state fb/close-page) + nil) - :addCircle - (fn [params] - (try - (let [params (-> params - (json/->clj) - (assoc :type :circle) - (fb/decode-shape))] - (vswap! state* fb/add-shape params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + :addBoard + (fn [params] + (try + (let [params (-> (decode-params params) + (assoc :type :frame) + (fb/decode-shape))] + (-> (swap! state fb/add-board params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) - :addPath - (fn [params] - (try - (let [params (-> params - (json/->clj) - (assoc :type :path) - (fb/decode-shape))] - (vswap! state* fb/add-shape params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + :closeBoard + (fn [] + (swap! state fb/close-board) + nil) - :addText - (fn [params] - (try - (let [params (-> params - (json/->clj) - (assoc :type :text) - (fb/decode-shape))] - (vswap! state* fb/add-shape params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + :addGroup + (fn [params] + (try + (let [params (-> (decode-params params) + (assoc :type :group) + (fb/decode-shape))] + (-> (swap! state fb/add-group params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) - :addLibraryColor - (fn [params] - (try - (let [params (-> params - (json/->clj) - (fb/decode-library-color) - (d/without-nils))] - (vswap! state* fb/add-library-color params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + :closeGroup + (fn [] + (swap! state fb/close-group) + nil) - :addLibraryTypography - (fn [params] - (try - (let [params (-> params - (json/->clj) - (fb/decode-library-typography) - (d/without-nils))] - (vswap! state* fb/add-library-typography params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + :addBool + (fn [params] + (try + (let [params (-> (decode-params params) + (fb/decode-add-bool))] + (-> (swap! state fb/add-bool params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) - :addComponent - (fn [params] - (try - (let [params (-> params - (json/->clj) - (fb/decode-component) - (d/without-nils))] - (vswap! state* fb/add-component params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + :addRect + (fn [params] + (try + (let [params (-> (decode-params params) + (assoc :type :rect) + (fb/decode-shape))] + (-> (swap! state fb/add-shape params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) - :addComponentInstance - (fn [params] - (try - (let [params (-> params - (json/->clj) - (fb/decode-add-component-instance) - (d/without-nils))] - (vswap! state* fb/add-component-instance params) - (dm/str (::fb/last-id @state*))) - (catch :default cause - (handle-exception cause)))) + :addCircle + (fn [params] + (try + (let [params (-> (decode-params params) + (assoc :type :circle) + (fb/decode-shape))] + (-> (swap! state fb/add-shape params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) - :getShape - (fn [shape-id] - (let [shape-id (uuid/parse shape-id)] - (some-> (fb/lookup-shape @state* shape-id) - (json/->js)))) + :addPath + (fn [params] + (try + (let [params (-> (decode-params params) + (assoc :type :path) + (fb/decode-shape))] + (-> (swap! state fb/add-shape params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) - :toMap - (fn [] - (-> @state* - (d/without-qualified) - (json/->js))))] + :addText + (fn [params] + (try + (let [params (-> (decode-params params) + (assoc :type :text) + (fb/decode-shape))] + (-> (swap! state fb/add-shape params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) + + :addLibraryColor + (fn [params] + (try + (let [params (-> (decode-params params) + (fb/decode-library-color) + (d/without-nils))] + (-> (swap! state fb/add-library-color params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) + + :addLibraryTypography + (fn [params] + (try + (let [params (-> (decode-params params) + (fb/decode-library-typography) + (d/without-nils))] + (-> (swap! state fb/add-library-typography params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) + + :addComponent + (fn [params] + (try + (let [params (-> (decode-params params) + (fb/decode-add-component))] + (-> (swap! state fb/add-component params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) + + :addComponentInstance + (fn [params] + (try + (let [params (-> (decode-params params) + (fb/decode-add-component-instance))] + (-> (swap! state fb/add-component-instance params) + (get-last-id))) + (catch :default cause + (handle-exception cause)))) + + :addFileMedia + (fn [params blob] + + (when-not (instance? js/Blob blob) + (throw (BuilderError. "validation" + "invalid-media" + "only Blob instance are soported"))) + (try + (let [blob (fb/map->BlobWrapper + {:size (.-size ^js blob) + :mtype (.-type ^js blob) + :blob blob}) + params + (-> (decode-params params) + (fb/decode-add-file-media))] + + (-> (swap! state fb/add-file-media params blob) + (get-last-id))) + + (catch :default cause + (handle-exception cause)))) + + :getMediaAsImage + (fn [id] + (let [id (uuid/parse id)] + (when-let [fmedia (get-in @state [::fb/file-media id])] + (let [image {:id (get fmedia :id) + :width (get fmedia :width) + :height (get fmedia :height) + :name (get fmedia :name) + :mtype (get fmedia :mtype)}] + (json/->js (d/without-nils image)))))) + + :genId + (fn [] + (dm/str (uuid/next))))) + + +(defn create-build-context + "Create an empty builder state context." + [] + (let [state (atom {}) + api (create-builder-api state)] (specify! api cljs.core/IDeref - (-deref [_] - (d/without-qualified @state*))))) - -(defn create-file - [params] - (try - (let [params (-> params json/->clj fb/decode-file) - file (fb/create-file params)] - (create-file-api file)) - (catch :default cause - (handle-exception cause)))) + (-deref [_] @state)))) diff --git a/library/src/lib/export.cljs b/library/src/lib/export.cljs index f777e14c60..f2b7fec266 100644 --- a/library/src/lib/export.cljs +++ b/library/src/lib/export.cljs @@ -8,12 +8,10 @@ "A .penpot export implementation" (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.files.builder :as fb] [app.common.json :as json] + [app.common.media :as media] [app.common.schema :as sm] - [app.common.uuid :as uuid] - [app.util.object :as obj] [app.common.types.color :as types.color] [app.common.types.component :as types.component] [app.common.types.file :as types.file] @@ -22,8 +20,8 @@ [app.common.types.shape :as types.shape] [app.common.types.tokens-lib :as types.tokens-lib] [app.common.types.typography :as types.typography] - [cuerdas.core :as str] [app.util.zip :as zip] + [cuerdas.core :as str] [promesa.core :as p])) (def ^:private schema:file @@ -43,9 +41,6 @@ (def ^:private encode-component (sm/encoder types.component/schema:component sm/json-transformer)) -;; (def encode-media -;; (sm/encoder ::ctf/media sm/json-transformer)) - (def encode-color (sm/encoder types.color/schema:color sm/json-transformer)) @@ -68,7 +63,6 @@ "file-data-fragment" "file-change"}) -;; FIXME: move to types (def ^:private schema:storage-object [:map {:title "StorageObject"} [:id ::sm/uuid] @@ -80,12 +74,6 @@ (def encode-storage-object (sm/encoder schema:storage-object sm/json-transformer)) -;; (def encode-file-thumbnail -;; (sm/encoder schema:file-thumbnail sm/json-transformer)) - - -;; FIXME: naming - (def ^:private file-attrs #{:id :name @@ -96,8 +84,6 @@ (defn- generate-file-export-procs [{:keys [id data] :as file}] - ;; (prn "generate-file-export-procs") - ;; (app.common.pprint/pprint file) (cons (let [file (cond-> (select-keys file file-attrs) (:options data) @@ -108,11 +94,11 @@ (concat (let [pages (get data :pages) pages-index (get data :pages-index)] + (->> (d/enumerate pages) (mapcat (fn [[index page-id]] - (let [path (str "files/" id "/pages/" page-id ".json") - page (get pages-index page-id) + (let [page (get pages-index page-id) objects (:objects page) page (-> page (dissoc :objects) @@ -155,45 +141,74 @@ (when-let [tokens-lib (get data :tokens-lib)] (list [(str "files/" id "/tokens.json") (delay (-> tokens-lib - (encode-tokens-lib tokens-lib) + encode-tokens-lib json/encode))]))))) -(defn generate-manifest-procs - [file] - (let [mdata {:id (:id file) - :name (:name file) - :features (:features file)} +(defn- generate-files-export-procs + [state] + (->> (vals (get state ::fb/files)) + (mapcat generate-file-export-procs))) + +(defn- generate-media-export-procs + [state] + (->> (get state ::fb/file-media) + (mapcat (fn [[file-media-id file-media]] + (let [media-id (get file-media :media-id) + media (get-in state [::fb/media media-id]) + blob (get-in state [::fb/blobs media-id])] + (list + [(str "objects/" media-id (media/mtype->extension (:content-type media))) + (delay (get blob :blob))] + + [(str "objects/" media-id ".json") + (delay (-> media + ;; FIXME: proper encode? + (json/encode)))] + [(str "files/" (:file-id file-media) "/media/" file-media-id ".json") + (delay (-> file-media + (dissoc :file-id) + (json/encode)))])))))) + +(defn- generate-manifest-procs + [state] + (let [files (->> (get state ::fb/files) + (mapv (fn [[file-id file]] + {:id file-id + :name (:name file) + :features (:features file)}))) params {:type "penpot/export-files" :version 1 + ;; FIXME: set proper placeholder for replacement on build :generated-by "penpot-lib/develop" - :files [mdata] + :files files :relations []}] - (list - ["manifest.json" (delay (json/encode params))]))) + ["manifest.json" (delay (json/encode params))])) (defn- export - [file writer] - (->> (p/reduce (fn [writer [path proc]] - (let [data (deref proc)] + [state writer] + (->> (p/reduce (fn [writer [path data]] + (let [data (if (delay? data) (deref data) data)] (js/console.log "export" path) (->> (zip/add writer path data) (p/fmap (constantly writer))))) writer - (concat - (generate-manifest-procs @file) - (generate-file-export-procs @file))) + (cons (generate-manifest-procs @state) + (concat + (generate-files-export-procs @state) + (generate-media-export-procs @state)))) + (p/mcat (fn [writer] (zip/close writer))))) (defn export-bytes - [file] - (export file (zip/writer (zip/bytes-writer)))) + [state] + (export state (zip/writer (zip/bytes-writer)))) (defn export-blob - [file] - (export file (zip/writer (zip/blob-writer)))) + [state] + (export state (zip/writer (zip/blob-writer)))) (defn export-stream - [file stream] - (export file (zip/writer stream))) + [state stream] + (export state (zip/writer stream))) diff --git a/library/yarn.lock b/library/yarn.lock index 67f82a9feb..44b445a74f 100644 --- a/library/yarn.lock +++ b/library/yarn.lock @@ -59,7 +59,6 @@ __metadata: concurrently: "npm:^9.1.2" luxon: "npm:^3.6.1" nodemon: "npm:^3.1.9" - sax: "npm:^1.4.1" shadow-cljs: "npm:3.0.5" source-map-support: "npm:^0.5.21" languageName: unknown @@ -73,11 +72,11 @@ __metadata: linkType: hard "@types/node@npm:^22.12.0": - version: 22.15.17 - resolution: "@types/node@npm:22.15.17" + version: 22.15.18 + resolution: "@types/node@npm:22.15.18" dependencies: undici-types: "npm:~6.21.0" - checksum: 10c0/fb92aa10b628683c5b965749f955bc2322485ecb0ea6c2f4cae5f2c7537a16834607e67083a9e9281faaae8d7dee9ada8d6a5c0de9a52c17d82912ef00c0fdd4 + checksum: 10c0/e23178c568e2dc6b93b6aa3b8dfb45f9556e527918c947fe7406a4c92d2184c7396558912400c3b1b8d0fa952ec63819aca2b8e4d3545455fc6f1e9623e09ca6 languageName: node linkType: hard @@ -318,14 +317,14 @@ __metadata: linkType: hard "debug@npm:4, debug@npm:^4, debug@npm:^4.3.4": - version: 4.4.0 - resolution: "debug@npm:4.4.0" + version: 4.4.1 + resolution: "debug@npm:4.4.1" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de + checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 languageName: node linkType: hard @@ -962,13 +961,6 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.4.1": - version: 1.4.1 - resolution: "sax@npm:1.4.1" - checksum: 10c0/6bf86318a254c5d898ede6bd3ded15daf68ae08a5495a2739564eb265cd13bcc64a07ab466fb204f67ce472bb534eb8612dac587435515169593f4fffa11de7c - languageName: node - linkType: hard - "semver@npm:^7.3.5, semver@npm:^7.5.3": version: 7.7.2 resolution: "semver@npm:7.7.2" diff --git a/package.json b/package.json index 1ef10821c7..2c2b65f982 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,14 @@ }, "type": "module", "scripts": { - "fmt:clj:check": "cljfmt check --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/", - "fmt:clj": "cljfmt fix --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/", + "fmt:clj:check": "cljfmt check --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/ library/src", + "fmt:clj": "cljfmt fix --parallel=true common/src/ common/test/ frontend/src/ frontend/test/ backend/src/ backend/test/ exporter/src/ library/src", "lint:clj:common": "clj-kondo --parallel=true --lint common/src", "lint:clj:frontend": "clj-kondo --parallel=true --lint frontend/src", "lint:clj:backend": "clj-kondo --parallel=true --lint backend/src", "lint:clj:exporter": "clj-kondo --parallel=true --lint exporter/src", - "lint:clj": "yarn run lint:clj:common && yarn run lint:clj:frontend && yarn run lint:clj:backend && yarn run lint:clj:exporter" + "lint:clj:library": "clj-kondo --parallel=true --lint library/src", + "lint:clj": "yarn run lint:clj:common && yarn run lint:clj:frontend && yarn run lint:clj:backend && yarn run lint:clj:exporter && yarn run lint:clj:library" }, "devDependencies": { "@playwright/test": "^1.43.1",