diff --git a/.gitignore b/.gitignore index ad4be629b..416950e13 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,8 @@ /vendor/**/target /vendor/svgclean/bundle*.js /web +/library/target/ + clj-profiler/ node_modules /test-results/ diff --git a/CHANGES.md b/CHANGES.md index 263f75f92..c091a73da 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/migrations.clj b/backend/src/app/binfile/migrations.clj index 62d180b49..0b524c3a9 100644 --- a/backend/src/app/binfile/migrations.clj +++ b/backend/src/app/binfile/migrations.clj @@ -10,7 +10,6 @@ [app.binfile.common :as bfc] [app.common.exceptions :as ex] [app.common.features :as cfeat] - [app.features.components-v2 :as feat.compv2] [clojure.set :as set] [cuerdas.core :as str])) @@ -28,13 +27,11 @@ (defn apply-pending-migrations! "Apply alredy registered pending migrations to files" - [cfg] - (doseq [[feature file-id] (-> bfc/*state* deref :pending-to-migrate)] + [_cfg] + (doseq [[feature _file-id] (-> bfc/*state* deref :pending-to-migrate)] (case feature "components/v2" - (feat.compv2/migrate-file! cfg file-id - :validate? (::validate cfg true) - :skip-on-graphic-error? true) + nil "fdata/shape-data-type" nil diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index 961dd2f33..7659ec567 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/src/app/features/components_v2.clj b/backend/src/app/features/components_v2.clj deleted file mode 100644 index f4e88ddd8..000000000 --- a/backend/src/app/features/components_v2.clj +++ /dev/null @@ -1,1845 +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 INC - -(ns app.features.components-v2 - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.exceptions :as ex] - [app.common.files.changes :as cp] - [app.common.files.changes-builder :as fcb] - [app.common.files.helpers :as cfh] - [app.common.files.migrations :as fmg] - [app.common.files.shapes-builder :as sbuilder] - [app.common.files.shapes-helpers :as cfsh] - [app.common.files.validate :as cfv] - [app.common.fressian :as fres] - [app.common.geom.matrix :as gmt] - [app.common.geom.point :as gpt] - [app.common.geom.rect :as grc] - [app.common.geom.shapes :as gsh] - [app.common.logging :as l] - [app.common.logic.libraries :as cll] - [app.common.math :as mth] - [app.common.schema :as sm] - [app.common.svg :as csvg] - [app.common.types.color :as ctc] - [app.common.types.component :as ctk] - [app.common.types.components-list :as ctkl] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.types.grid :as ctg] - [app.common.types.modifiers :as ctm] - [app.common.types.page :as ctp] - [app.common.types.pages-list :as ctpl] - [app.common.types.path :as path] - [app.common.types.shape :as cts] - [app.common.types.shape-tree :as ctst] - [app.common.types.shape.text :as ctsx] - [app.common.uuid :as uuid] - [app.config :as cf] - [app.db :as db] - [app.db.sql :as sql] - [app.features.fdata :as fdata] - [app.media :as media] - [app.rpc.commands.files :as files] - [app.rpc.commands.files-snapshot :as fsnap] - [app.rpc.commands.media :as cmd.media] - [app.storage :as sto] - [app.storage.impl :as impl] - [app.storage.tmp :as tmp] - [app.svgo :as svgo] - [app.util.blob :as blob] - [app.util.pointer-map :as pmap] - [app.util.time :as dt] - [buddy.core.codecs :as bc] - [clojure.set :refer [rename-keys]] - [cuerdas.core :as str] - [datoteka.fs :as fs] - [datoteka.io :as io] - [promesa.util :as pu])) - - -(def ^:dynamic *stats* - "A dynamic var for setting up state for collect stats globally." - nil) - -(def ^:dynamic *cache* - "A dynamic var for setting up a cache instance." - false) - -(def ^:dynamic *skip-on-graphic-error* - "A dynamic var for setting up the default error behavior for graphics processing." - nil) - -(def ^:dynamic ^:private *system* - "An internal var for making the current `system` available to all - internal functions without the need to explicitly pass it top down." - nil) - -(def ^:dynamic ^:private *file-stats* - "An internal dynamic var for collect stats by file." - nil) - -(def ^:dynamic ^:private *team-stats* - "An internal dynamic var for collect stats by team." - nil) - -(def grid-gap 50) -(def frame-gap 200) -(def max-group-size 50) - -(defn decode-row - [{:keys [features data] :as row}] - (cond-> row - (some? features) - (assoc :features (db/decode-pgarray features #{})) - - (some? data) - (assoc :data (blob/decode data)))) - -(set! *warn-on-reflection* true) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; FILE PREPARATION BEFORE MIGRATION -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(def valid-recent-color? - (sm/lazy-validator ::ctc/recent-color)) - -(def valid-color? - (sm/lazy-validator ::ctc/color)) - -(def valid-fill? - (sm/lazy-validator cts/schema:fill)) - -(def valid-stroke? - (sm/lazy-validator ::cts/stroke)) - -(def valid-flow? - (sm/lazy-validator ::ctp/flow)) - -(def valid-text-content? - (sm/lazy-validator ::ctsx/content)) - -(def valid-path-content? - (sm/lazy-validator ::path/segments)) - -(def valid-path-segment? - (sm/lazy-validator ::path/segment)) - -(def valid-rgb-color-string? - (sm/lazy-validator ::ctc/rgb-color)) - -(def valid-shape-points? - (sm/lazy-validator cts/schema:points)) - -(def valid-image-attrs? - (sm/lazy-validator cts/schema:image-attrs)) - -(def valid-column-grid-params? - (sm/lazy-validator ::ctg/column-params)) - -(def valid-square-grid-params? - (sm/lazy-validator ::ctg/square-params)) - - -(defn- prepare-file-data - "Apply some specific migrations or fixes to things that are allowed in v1 but not in v2, - or that are the result of old bugs." - [file-data libraries] - (let [detached-ids (volatile! #{}) - - detach-shape - (fn [container shape] - ;; Detach a shape and make necessary adjustments. - (let [is-component? (let [root-shape (ctst/get-shape container (:id container))] - (and (some? root-shape) (nil? (:parent-id root-shape)))) - parent (ctst/get-shape container (:parent-id shape)) - in-copy? (ctn/in-any-component? (:objects container) parent)] - - (letfn [(detach-recursive [container shape first?] - - ;; If the shape is inside a component, add it to detached-ids. This list is used - ;; later to process other copies that was referencing a detached nested copy. - (when is-component? - (vswap! detached-ids conj (:id shape))) - - ;; Detach the shape and all children until we find a subinstance. - (if (or first? in-copy? (not (ctk/instance-head? shape))) - (as-> container $ - (ctn/update-shape $ (:id shape) ctk/detach-shape) - (reduce #(detach-recursive %1 %2 false) - $ - (map (d/getf (:objects container)) (:shapes shape)))) - - ;; If this is a subinstance head and the initial shape whas not itself a - ;; nested copy, stop detaching and promote it to root. - (ctn/update-shape container (:id shape) #(assoc % :component-root true))))] - - (detach-recursive container shape true)))) - - fix-bad-children - (fn [file-data] - ;; Remove any child that does not exist. And also remove duplicated children. - (letfn [(fix-container [container] - (d/update-when container :objects update-vals (partial fix-shape container))) - - (fix-shape [container shape] - (let [objects (:objects container)] - (d/update-when shape :shapes - (fn [shapes] - (->> shapes - (d/removev #(nil? (get objects %))) - (into [] (distinct)))))))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-missing-image-metadata - (fn [file-data] - ;; Delete broken image shapes with no metadata. - (letfn [(fix-container [container] - (d/update-when container :objects #(reduce-kv fix-shape % %))) - - (fix-shape [objects id shape] - (if (and (cfh/image-shape? shape) - (nil? (:metadata shape))) - (-> objects - (dissoc id) - (d/update-in-when [(:parent-id shape) :shapes] - (fn [shapes] (filterv #(not= id %) shapes)))) - objects))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-invalid-page - (fn [file-data] - (letfn [(update-page [page] - (-> page - (update :name (fn [name] - (if (nil? name) - "Page" - name))) - (update :options fix-options))) - - (fix-background [options] - (if (and (contains? options :background) - (not (valid-rgb-color-string? (:background options)))) - (dissoc options :background) - options)) - - (fix-saved-grids [options] - (d/update-when options :saved-grids - (fn [grids] - (cond-> grids - (and (contains? grids :column) - (not (valid-column-grid-params? (:column grids)))) - (dissoc :column) - - (and (contains? grids :row) - (not (valid-column-grid-params? (:row grids)))) - (dissoc :row) - - (and (contains? grids :square) - (not (valid-square-grid-params? (:square grids)))) - (dissoc :square))))) - - (fix-options [options] - (-> options - ;; Some pages has invalid data on flows, we proceed just to - ;; delete them. - (d/update-when :flows #(filterv valid-flow? %)) - (fix-saved-grids) - (fix-background)))] - - (update file-data :pages-index update-vals update-page))) - - ;; Sometimes we found that the file has issues in the internal - ;; data structure of the local library; this function tries to - ;; fix that issues. - fix-file-data - (fn [file-data] - (letfn [(fix-colors-library [colors] - (let [colors (dissoc colors nil)] - (reduce-kv (fn [colors id color] - (if (valid-color? color) - colors - (dissoc colors id))) - colors - colors)))] - (-> file-data - (d/update-when :colors fix-colors-library) - (d/update-when :typographies dissoc nil)))) - - fix-big-geometry-shapes - (fn [file-data] - ;; At some point in time, we had a bug that generated shapes - ;; with huge geometries that did not validate the - ;; schema. Since we don't have a way to fix those shapes, we - ;; simply proceed to delete it. We ignore path type shapes - ;; because they have not been affected by the bug. - (letfn [(fix-container [container] - (d/update-when container :objects #(reduce-kv fix-shape % %))) - - (fix-shape [objects id shape] - (cond - (or (cfh/path-shape? shape) - (cfh/bool-shape? shape)) - objects - - (or (and (number? (:x shape)) (not (sm/valid-safe-number? (:x shape)))) - (and (number? (:y shape)) (not (sm/valid-safe-number? (:y shape)))) - (and (number? (:width shape)) (not (sm/valid-safe-number? (:width shape)))) - (and (number? (:height shape)) (not (sm/valid-safe-number? (:height shape))))) - (-> objects - (dissoc id) - (d/update-in-when [(:parent-id shape) :shapes] - (fn [shapes] (filterv #(not= id %) shapes)))) - - :else - objects))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - ;; Some files has totally broken shapes, we just remove them - fix-completly-broken-shapes - (fn [file-data] - (letfn [(update-object [objects id shape] - (cond - (nil? (:type shape)) - (let [ids (cfh/get-children-ids objects id)] - (-> objects - (dissoc id) - (as-> $ (reduce dissoc $ ids)) - (d/update-in-when [(:parent-id shape) :shapes] - (fn [shapes] (filterv #(not= id %) shapes))))) - - (and (cfh/text-shape? shape) - (not (valid-text-content? (:content shape)))) - (dissoc objects id) - - (and (cfh/path-shape? shape) - (not (valid-path-content? (:content shape)))) - (-> objects - (dissoc id) - (d/update-in-when [(:parent-id shape) :shapes] - (fn [shapes] (filterv #(not= id %) shapes)))) - - :else - objects)) - - (update-container [container] - (d/update-when container :objects #(reduce-kv update-object % %)))] - - (-> file-data - (update :pages-index update-vals update-container) - (update :components update-vals update-container)))) - - fix-shape-geometry - (fn [file-data] - (letfn [(fix-container [container] - (d/update-when container :objects update-vals fix-shape)) - - (fix-shape [shape] - (cond - (and (cfh/image-shape? shape) - (valid-image-attrs? shape) - (grc/valid-rect? (:selrect shape)) - (not (valid-shape-points? (:points shape)))) - (let [selrect (:selrect shape) - metadata (:metadata shape) - selrect (grc/make-rect - (:x selrect) - (:y selrect) - (:width metadata) - (:height metadata)) - points (grc/rect->points selrect)] - (assoc shape - :selrect selrect - :points points)) - - (and (cfh/text-shape? shape) - (valid-text-content? (:content shape)) - (not (valid-shape-points? (:points shape))) - (seq (:position-data shape))) - (let [selrect (->> (:position-data shape) - (map (juxt :x :y :width :height)) - (map #(apply grc/make-rect %)) - (grc/join-rects)) - points (grc/rect->points selrect)] - - (assoc shape - :x (:x selrect) - :y (:y selrect) - :width (:width selrect) - :height (:height selrect) - :selrect selrect - :points points)) - - (and (cfh/text-shape? shape) - (valid-text-content? (:content shape)) - (not (valid-shape-points? (:points shape))) - (grc/valid-rect? (:selrect shape))) - (let [selrect (:selrect shape) - points (grc/rect->points selrect)] - (assoc shape - :x (:x selrect) - :y (:y selrect) - :width (:width selrect) - :height (:height selrect) - :points points)) - - (and (or (cfh/rect-shape? shape) - (cfh/svg-raw-shape? shape) - (cfh/circle-shape? shape)) - (not (valid-shape-points? (:points shape))) - (grc/valid-rect? (:selrect shape))) - (let [selrect (if (grc/valid-rect? (:svg-viewbox shape)) - (:svg-viewbox shape) - (:selrect shape)) - points (grc/rect->points selrect)] - (assoc shape - :x (:x selrect) - :y (:y selrect) - :width (:width selrect) - :height (:height selrect) - :selrect selrect - :points points)) - - (and (= :icon (:type shape)) - (grc/valid-rect? (:selrect shape)) - (valid-shape-points? (:points shape))) - (-> shape - (assoc :type :rect) - (dissoc :content) - (dissoc :metadata) - (dissoc :segments) - (dissoc :x1 :y1 :x2 :y2)) - - (and (cfh/group-shape? shape) - (grc/valid-rect? (:selrect shape)) - (not (valid-shape-points? (:points shape)))) - (assoc shape :points (grc/rect->points (:selrect shape))) - - :else - shape))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-empty-components - (fn [file-data] - (letfn [(fix-component [components id component] - (let [root-shape (ctst/get-shape component (:id component))] - (if (or (empty? (:objects component)) - (nil? root-shape) - (nil? (:type root-shape))) - (dissoc components id) - components)))] - - (-> file-data - (d/update-when :components #(reduce-kv fix-component % %))))) - - fix-components-with-component-root - ;;In v1 no components in the library should have component-root - (fn [file-data] - (letfn [(fix-container [container] - (d/update-when container :objects update-vals fix-shape)) - - (fix-shape [shape] - (dissoc shape :component-root))] - - (-> file-data - (update :components update-vals fix-container)))) - - fix-non-existing-component-ids - ;; Check component ids have valid values. - (fn [file-data] - (let [libraries (assoc-in libraries [(:id file-data) :data] file-data)] - (letfn [(fix-container [container] - (d/update-when container :objects update-vals fix-shape)) - - (fix-shape [shape] - (let [component-id (:component-id shape) - component-file (:component-file shape) - library (get libraries component-file)] - - (cond-> shape - (and (some? component-id) - (some? library) - (nil? (ctkl/get-component (:data library) component-id))) - (ctk/detach-shape))))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container))))) - - fix-misc-shape-issues - (fn [file-data] - (letfn [(fix-container [container] - (d/update-when container :objects update-vals fix-shape)) - - (fix-gap-value [gap] - (if (or (= gap ##Inf) - (= gap ##-Inf)) - 0 - gap)) - - (fix-shape [shape] - (cond-> shape - ;; Some shapes has invalid gap value - (contains? shape :layout-gap) - (update :layout-gap (fn [layout-gap] - (if (number? layout-gap) - {:row-gap layout-gap :column-gap layout-gap} - (-> layout-gap - (d/update-when :column-gap fix-gap-value) - (d/update-when :row-gap fix-gap-value))))) - - ;; Fix name if missing - (nil? (:name shape)) - (assoc :name (d/name (:type shape))) - - ;; Remove v2 info from components that have been copied and pasted - ;; from a v2 file - (some? (:main-instance shape)) - (dissoc :main-instance) - - (and (contains? shape :transform) - (not (gmt/valid-matrix? (:transform shape)))) - (assoc :transform (gmt/matrix)) - - (and (contains? shape :transform-inverse) - (not (gmt/valid-matrix? (:transform-inverse shape)))) - (assoc :transform-inverse (gmt/matrix)) - - ;; Fix broken fills - (seq (:fills shape)) - (update :fills (fn [fills] (filterv valid-fill? fills))) - - ;; Fix broken strokes - (seq (:strokes shape)) - (update :strokes (fn [strokes] (filterv valid-stroke? strokes))) - - ;; Fix some broken layout related attrs, probably - ;; of copypaste on flex layout betatest period - (true? (:layout shape)) - (assoc :layout :flex)))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - ;; There are some bugs in the past that allows convert text to - ;; path and this fix tries to identify this cases and fix them converting - ;; the shape back to text shape - - fix-text-shapes-converted-to-path - (fn [file-data] - (letfn [(fix-container [container] - (d/update-when container :objects update-vals fix-shape)) - - (fix-shape [shape] - (if (and (cfh/path-shape? shape) - (contains? shape :content) - (some? (:selrect shape)) - (valid-text-content? (:content shape))) - (let [selrect (:selrect shape)] - (-> shape - (assoc :x (:x selrect)) - (assoc :y (:y selrect)) - (assoc :width (:width selrect)) - (assoc :height (:height selrect)) - (assoc :type :text))) - shape))] - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-broken-paths - (fn [file-data] - (letfn [(fix-container [container] - (d/update-when container :objects update-vals fix-shape)) - - (fix-shape [shape] - (cond - (and (cfh/path-shape? shape) - (seq (:content shape)) - (not (valid-path-content? (:content shape)))) - (let [shape (update shape :content fix-path-content)] - (if (not (valid-path-content? (:content shape))) - shape - (-> shape - (dissoc :bool-content) - (dissoc :bool-type) - (path/update-geometry)))) - - ;; When we fount a bool shape with no content, - ;; we convert it to a simple rect - (and (cfh/bool-shape? shape) - (not (seq (:bool-content shape)))) - (let [selrect (or (:selrect shape) - (grc/make-rect)) - points (grc/rect->points selrect)] - (-> shape - (assoc :x (:x selrect)) - (assoc :y (:y selrect)) - (assoc :width (:height selrect)) - (assoc :height (:height selrect)) - (assoc :selrect selrect) - (assoc :points points) - (assoc :type :rect) - (assoc :transform (gmt/matrix)) - (assoc :transform-inverse (gmt/matrix)) - (dissoc :bool-content) - (dissoc :shapes) - (dissoc :content))) - - :else - shape)) - - (fix-path-content [content] - (let [[seg1 :as content] (filterv valid-path-segment? content)] - (if (and seg1 (not= :move-to (:command seg1))) - (let [params (select-keys (:params seg1) [:x :y])] - (into [{:command :move-to :params params}] content)) - content)))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-recent-colors - (fn [file-data] - ;; Remove invalid colors in :recent-colors - (d/update-when file-data :recent-colors - (fn [colors] - (filterv valid-recent-color? colors)))) - - fix-broken-parents - (fn [file-data] - ;; Find children shapes whose parent-id is not set to the parent that contains them. - ;; Remove them from the parent :shapes list. - (letfn [(fix-container [container] - (d/update-when container :objects #(reduce-kv fix-shape % %))) - - (fix-shape [objects id shape] - (reduce (fn [objects child-id] - (let [child (get objects child-id)] - (cond-> objects - (and (some? child) (not= id (:parent-id child))) - (d/update-in-when [id :shapes] - (fn [shapes] (filterv #(not= child-id %) shapes)))))) - objects - (:shapes shape)))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-orphan-shapes - (fn [file-data] - ;; Find shapes that are not listed in their parent's children list. - ;; Remove them, and also their children - (letfn [(fix-container [container] - (reduce fix-shape container (ctn/shapes-seq container))) - - (fix-shape - [container shape] - (if-not (or (= (:id shape) uuid/zero) - (nil? (:parent-id shape))) - (let [parent (ctst/get-shape container (:parent-id shape)) - exists? (d/index-of (:shapes parent) (:id shape))] - (if (nil? exists?) - (let [ids (cfh/get-children-ids-with-self (:objects container) (:id shape))] - (update container :objects #(reduce dissoc % ids))) - container)) - container))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - remove-nested-roots - (fn [file-data] - ;; Remove :component-root in head shapes that are nested. - (letfn [(fix-container [container] - (d/update-when container :objects update-vals (partial fix-shape container))) - - (fix-shape [container shape] - (let [parent (ctst/get-shape container (:parent-id shape))] - (if (and (ctk/instance-root? shape) - (ctn/in-any-component? (:objects container) parent)) - (dissoc shape :component-root) - shape)))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - add-not-nested-roots - (fn [file-data] - ;; Add :component-root in head shapes that are not nested. - (letfn [(fix-container [container] - (d/update-when container :objects update-vals (partial fix-shape container))) - - (fix-shape [container shape] - (let [parent (ctst/get-shape container (:parent-id shape))] - (if (and (ctk/subinstance-head? shape) - (not (ctn/in-any-component? (:objects container) parent))) - (assoc shape :component-root true) - shape)))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-orphan-copies - (fn [file-data] - ;; Detach shapes that were inside a copy (have :shape-ref) but now they aren't. - (letfn [(fix-container [container] - (reduce fix-shape container (ctn/shapes-seq container))) - - (fix-shape [container shape] - (let [shape (ctst/get-shape container (:id shape)) ; Get the possibly updated shape - parent (ctst/get-shape container (:parent-id shape))] - (if (and (ctk/in-component-copy? shape) - (not (ctk/instance-head? shape)) - (not (ctk/in-component-copy? parent))) - (detach-shape container shape) - container)))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-components-without-id - (fn [file-data] - ;; We have detected some components that have no :id attribute. - ;; Regenerate it from the components map. - (letfn [(fix-component [id component] - (if (some? (:id component)) - component - (assoc component :id id)))] - - (-> file-data - (d/update-when :components #(d/mapm fix-component %))))) - - remap-refs - (fn [file-data] - ;; Remap shape-refs so that they point to the near main. - ;; At the same time, if there are any dangling ref, detach the shape and its children. - (let [count (volatile! 0) - - fix-shape - (fn [container shape] - (if (ctk/in-component-copy? shape) - ;; First look for the direct shape. - (let [root (ctn/get-component-shape (:objects container) shape) - libraries (assoc-in libraries [(:id file-data) :data] file-data) - library (get libraries (:component-file root)) - component (ctkl/get-component (:data library) (:component-id root) true) - direct-shape (ctf/get-component-shape (:data library) component (:shape-ref shape))] - (if (some? direct-shape) - ;; If it exists, there is nothing else to do. - container - ;; If not found, find the near shape. - (let [near-shape (d/seek #(= (:shape-ref %) (:shape-ref shape)) - (ctf/get-component-shapes (:data library) component))] - (if (some? near-shape) - ;; If found, update the ref to point to the near shape. - (do - (vswap! count inc) - (ctn/update-shape container (:id shape) #(assoc % :shape-ref (:id near-shape)))) - ;; If not found, it may be a fostered component. Try to locate a direct shape - ;; in the head component. - (let [head (ctn/get-head-shape (:objects container) shape) - library-2 (get libraries (:component-file head)) - component-2 (ctkl/get-component (:data library-2) (:component-id head) true) - direct-shape-2 (ctf/get-component-shape (:data library-2) component-2 (:shape-ref shape))] - (if (some? direct-shape-2) - ;; If it exists, there is nothing else to do. - container - ;; If not found, detach shape and all children. - ;; container - (do - (vswap! count inc) - (detach-shape container shape)))))))) - container)) - - fix-container - (fn [container] - (reduce fix-shape container (ctn/shapes-seq container)))] - - [(-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)) - @count])) - - remap-refs-recur - ;; remapping refs can generate cascade changes so we call it until no changes are done - (fn [file-data] - (loop [f-data file-data] - (let [[f-data count] (remap-refs f-data)] - (if (= count 0) - f-data - (recur f-data))))) - - fix-converted-copies - (fn [file-data] - ;; If the user has created a copy and then converted into a path or bool, - ;; detach it because the synchronization will no longer work. - (letfn [(fix-container [container] - (reduce fix-shape container (ctn/shapes-seq container))) - - (fix-shape [container shape] - (if (and (ctk/instance-head? shape) - (or (cfh/path-shape? shape) - (cfh/bool-shape? shape))) - (detach-shape container shape) - container))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - wrap-non-group-component-roots - (fn [file-data] - ;; Some components have a root that is not a group nor a frame - ;; (e.g. a path or a svg-raw). We need to wrap them in a frame - ;; for this one to became the root. - (letfn [(fix-component [component] - (let [root-shape (ctst/get-shape component (:id component))] - (if (or (cfh/group-shape? root-shape) - (cfh/frame-shape? root-shape)) - component - (let [new-id (uuid/next) - frame (-> (cts/setup-shape - {:type :frame - :id (:id component) - :x (:x (:selrect root-shape)) - :y (:y (:selrect root-shape)) - :width (:width (:selrect root-shape)) - :height (:height (:selrect root-shape)) - :name (:name component) - :shapes [new-id] - :show-content true}) - (assoc :frame-id nil - :parent-id nil)) - root-shape' (assoc root-shape - :id new-id - :parent-id (:id frame) - :frame-id (:id frame))] - (update component :objects assoc - (:id frame) frame - (:id root-shape') root-shape')))))] - - (-> file-data - (d/update-when :components update-vals fix-component)))) - - detach-non-group-instance-roots - (fn [file-data] - ;; If there is a copy instance whose root is not a frame or a group, it cannot - ;; be easily repaired, and anyway it's not working in production, so detach it. - (letfn [(fix-container [container] - (reduce fix-shape container (ctn/shapes-seq container))) - - (fix-shape [container shape] - (if (and (ctk/instance-head? shape) - (not (#{:group :frame} (:type shape)))) - (detach-shape container shape) - container))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - transform-to-frames - (fn [file-data] - ;; Transform component and copy heads fron group to frames, and set the - ;; frame-id of its childrens - (letfn [(fix-container [container] - (d/update-when container :objects update-vals fix-shape)) - - (fix-shape [shape] - (if (or (nil? (:parent-id shape)) (ctk/instance-head? shape)) - (let [frame? (= :frame (:type shape)) - not-group? (not= :group (:type shape))] - (assoc shape ; Old groups must be converted - :type :frame ; to frames and conform to spec - :fills (if not-group? (d/nilv (:fills shape) []) []) ; Groups never should have fill - :shapes (or (:shapes shape) []) - :hide-in-viewer (if frame? (boolean (:hide-in-viewer shape)) true) - :show-content (if frame? (boolean (:show-content shape)) true) - :r1 (or (:r1 shape) 0) - :r2 (or (:r2 shape) 0) - :r3 (or (:r3 shape) 0) - :r4 (or (:r4 shape) 0))) - shape))] - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - remap-frame-ids - (fn [file-data] - ;; Remap the frame-ids of the primary childs of the head instances - ;; to point to the head instance. - (letfn [(fix-container - [container] - (d/update-when container :objects update-vals (partial fix-shape container))) - - (fix-shape - [container shape] - (let [parent (ctst/get-shape container (:parent-id shape))] - (if (ctk/instance-head? parent) - (assoc shape :frame-id (:id parent)) - shape)))] - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-frame-ids - (fn [file-data] - ;; Ensure that frame-id of all shapes point to the parent or to the frame-id - ;; of the parent, and that the destination is indeed a frame. - (letfn [(fix-container [container] - (d/update-when container :objects #(cfh/reduce-objects % fix-shape %))) - - (fix-shape [objects shape] - (let [parent (when (:parent-id shape) - (get objects (:parent-id shape))) - error? (when (some? parent) - (if (= (:type parent) :frame) - (not= (:frame-id shape) (:id parent)) - (not= (:frame-id shape) (:frame-id parent))))] - (if error? - (let [nearest-frame (cfh/get-frame objects (:parent-id shape)) - frame-id (or (:id nearest-frame) uuid/zero)] - (update objects (:id shape) assoc :frame-id frame-id)) - objects)))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-component-nil-objects - (fn [file-data] - ;; Ensure that objects of all components is not null - (letfn [(fix-component [component] - (if (and (contains? component :objects) (nil? (:objects component))) - (if (:deleted component) - (assoc component :objects {}) - (dissoc component :objects)) - component))] - (-> file-data - (d/update-when :components update-vals fix-component)))) - - fix-false-copies - (fn [file-data] - ;; Find component heads that are not main-instance but have not :shape-ref. - ;; Also shapes that have :shape-ref but are not in a copy. - (letfn [(fix-container [container] - (reduce fix-shape container (ctn/shapes-seq container))) - - (fix-shape - [container shape] - (if (or (and (ctk/instance-head? shape) - (not (ctk/main-instance? shape)) - (not (ctk/in-component-copy? shape))) - (and (ctk/in-component-copy? shape) - (nil? (ctn/get-head-shape (:objects container) shape {:allow-main? true})))) - (detach-shape container shape) - container))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - - fix-component-root-without-component - (fn [file-data] - ;; Ensure that if component-root is set component-file and component-id are set too - (letfn [(fix-container [container] - (d/update-when container :objects update-vals fix-shape)) - - (fix-shape [shape] - (cond-> shape - (and (ctk/instance-root? shape) - (or (not (ctk/instance-head? shape)) - (not (some? (:component-file shape))))) - (dissoc :component-id - :component-file - :component-root)))] - (-> file-data - (update :pages-index update-vals fix-container)))) - - - fix-copies-names - (fn [file-data] - ;; Rename component heads to add the component path to the name - (letfn [(fix-container [container] - (d/update-when container :objects #(cfh/reduce-objects % fix-shape %))) - - (fix-shape [objects shape] - (let [root (ctn/get-component-shape objects shape) - libraries (assoc-in libraries [(:id file-data) :data] file-data) - library (get libraries (:component-file root)) - component (ctkl/get-component (:data library) (:component-id root) true) - path (str/trim (:path component))] - (if (and (ctk/instance-head? shape) - (some? component) - (= (:name component) (:name shape)) - (not (str/empty? path))) - (update objects (:id shape) assoc :name (str path " / " (:name component))) - objects)))] - - (-> file-data - (update :pages-index update-vals fix-container)))) - - fix-copies-of-detached - (fn [file-data] - ;; Find any copy that is referencing a shape inside a component that have - ;; been detached in a previous fix. If so, undo the nested copy, converting - ;; it into a direct copy. - ;; - ;; WARNING: THIS SHOULD BE CALLED AT THE END OF THE PROCESS. - (letfn [(fix-container [container] - (reduce fix-shape container (ctn/shapes-seq container))) - (fix-shape [container shape] - (cond-> container - (@detached-ids (:shape-ref shape)) - (detach-shape shape)))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container))))] - - (-> file-data - (fix-file-data) - (fix-invalid-page) - (fix-misc-shape-issues) - (fix-recent-colors) - (fix-missing-image-metadata) - (fix-text-shapes-converted-to-path) - (fix-broken-paths) - (fix-big-geometry-shapes) - (fix-shape-geometry) - (fix-empty-components) - (fix-components-with-component-root) - (fix-non-existing-component-ids) - (fix-completly-broken-shapes) - (fix-bad-children) - (fix-broken-parents) - (fix-orphan-shapes) - (fix-orphan-copies) - (remove-nested-roots) - (add-not-nested-roots) - (fix-components-without-id) - (fix-converted-copies) - (remap-refs-recur) - (wrap-non-group-component-roots) - (detach-non-group-instance-roots) - (transform-to-frames) - (remap-frame-ids) - (fix-frame-ids) - (fix-component-nil-objects) - (fix-false-copies) - (fix-component-root-without-component) - (fix-copies-names) - (fix-copies-of-detached)))); <- Do not add fixes after this and fix-orphan-copies call - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; COMPONENTS MIGRATION -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- get-asset-groups - [assets generic-name] - (let [;; Group by first element of the path. - groups (d/group-by #(first (cfh/split-path (:path %))) assets) - ;; If there is a group called as the generic-name we have to preserve it - unames (into #{} (keep str) (keys groups)) - groups (rename-keys groups {generic-name (cfh/generate-unique-name generic-name unames)}) - - ;; Split large groups in chunks of max-group-size elements - groups (loop [groups (seq groups) - result {}] - (if (empty? groups) - result - (let [[group-name assets] (first groups) - group-name (if (or (nil? group-name) (str/empty? group-name)) - generic-name - group-name)] - (if (<= (count assets) max-group-size) - (recur (next groups) - (assoc result group-name assets)) - (let [splits (-> (partition-all max-group-size assets) - (d/enumerate))] - (recur (next groups) - (reduce (fn [result [index split]] - (let [split-name (str group-name " " (inc index))] - (assoc result split-name split))) - result - splits))))))) - - ;; Sort assets in each group by path - groups (update-vals groups (fn [assets] - (sort-by (fn [{:keys [path name]}] - (str/lower (cfh/merge-path-item path name))) - assets)))] - - ;; Sort groups by name - (into (sorted-map) groups))) - -(defn- create-frame - [name position width height] - (cts/setup-shape - {:type :frame - :x (:x position) - :y (:y position) - :width (+ width grid-gap) - :height (+ height grid-gap) - :name name - :frame-id uuid/zero - :parent-id uuid/zero})) - -(defn- migrate-components - "If there is any component in the file library, add a new 'Library - backup', generate main instances for all components there and remove - shapes from library components. Mark the file with the :components-v2 option." - [file-data libraries] - (let [file-data (prepare-file-data file-data libraries) - components (ctkl/components-seq file-data)] - (if (empty? components) - (assoc-in file-data [:options :components-v2] true) - (let [[file-data page-id start-pos] - (ctf/get-or-add-library-page file-data frame-gap) - - migrate-component-shape - (fn [shape delta component-file component-id frame-id] - (cond-> shape - (nil? (:parent-id shape)) - (assoc :parent-id frame-id - :main-instance true - :component-root true - :component-file component-file - :component-id component-id) - - (nil? (:frame-id shape)) - (assoc :frame-id frame-id) - - :always - (gsh/move delta))) - - add-main-instance - (fn [file-data component frame-id position] - (let [shapes (cfh/get-children-with-self (:objects component) - (:id component)) - - ;; Let's calculate the top shame name from the components path and name - root-shape (-> (first shapes) - (assoc :name (cfh/merge-path-item (:path component) (:name component)))) - - shapes (assoc shapes 0 root-shape) - - orig-pos (gpt/point (:x root-shape) (:y root-shape)) - delta (gpt/subtract position orig-pos) - - xf-shape (map #(migrate-component-shape % - delta - (:id file-data) - (:id component) - frame-id)) - new-shapes - (into [] xf-shape shapes) - - find-frame-id ; if its parent is a frame, the frame-id should be the parent-id - (fn [page shape] - (let [parent (ctst/get-shape page (:parent-id shape))] - (if (= :frame (:type parent)) - (:id parent) - (:frame-id parent)))) - - add-shapes - (fn [page] - (reduce (fn [page shape] - (ctst/add-shape (:id shape) - shape - page - (find-frame-id page shape) - (:parent-id shape) - nil ; <- As shapes are ordered, we can safely add each - true)) ; one at the end of the parent's children list. - page - new-shapes)) - - update-component - (fn [component] - (-> component - (assoc :main-instance-id (:id root-shape) - :main-instance-page page-id) - (dissoc :objects)))] - - (-> file-data - (ctpl/update-page page-id add-shapes) - (ctkl/update-component (:id component) update-component)))) - - add-instance-grid - (fn [fdata frame-id grid assets] - (reduce (fn [result [component position]] - (add-main-instance result component frame-id (gpt/add position - (gpt/point grid-gap grid-gap)))) - fdata - (d/zip assets grid))) - - add-instance-grids - (fn [fdata] - (let [components (ctkl/components-seq fdata) - groups (get-asset-groups components "Components")] - (loop [groups (seq groups) - fdata fdata - position start-pos] - (if (empty? groups) - fdata - (let [[group-name assets] (first groups) - grid (ctst/generate-shape-grid - (map (partial ctf/get-component-root fdata) assets) - position - grid-gap) - {:keys [width height]} (meta grid) - frame (create-frame group-name position width height) - fdata (ctpl/update-page fdata - page-id - #(ctst/add-shape (:id frame) - frame - % - (:id frame) - (:id frame) - nil - true))] - (recur (next groups) - (add-instance-grid fdata (:id frame) grid assets) - (gpt/add position (gpt/point 0 (+ height (* 2 grid-gap) frame-gap)))))))))] - - (let [total (count components)] - (some-> *stats* (swap! update :processed-components (fnil + 0) total)) - (some-> *team-stats* (swap! update :processed-components (fnil + 0) total)) - (some-> *file-stats* (swap! assoc :processed-components total))) - - (add-instance-grids file-data))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; GRAPHICS MIGRATION -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- create-shapes-for-bitmap - "Convert a media object that contains a bitmap image into shapes, - one shape of type :image and one group that contains it." - [{:keys [name width height id mtype]} frame-id position] - (let [frame-shape (-> (cts/setup-shape - {:type :frame - :x (:x position) - :y (:y position) - :width width - :height height - :name name - :frame-id frame-id - :parent-id frame-id}) - (assoc - :proportion (float (/ width height)) - :proportion-lock true)) - - img-shape (cts/setup-shape - {:type :image - :x (:x position) - :y (:y position) - :width width - :height height - :metadata {:id id - :width width - :height height - :mtype mtype} - :name name - :frame-id (:id frame-shape) - :parent-id (:id frame-shape) - :constraints-h :scale - :constraints-v :scale})] - [frame-shape [img-shape]])) - -(defn- parse-datauri - [data] - (let [[mtype b64-data] (str/split data ";base64," 2) - mtype (subs mtype (inc (str/index-of mtype ":"))) - data (-> b64-data bc/str->bytes bc/b64->bytes)] - [mtype data])) - -(defn- extract-name - [href] - (let [query-idx (d/nilv (str/last-index-of href "?") 0) - href (if (> query-idx 0) (subs href 0 query-idx) href) - filename (->> (str/split href "/") (last)) - ext-idx (str/last-index-of filename ".")] - (if (> ext-idx 0) (subs filename 0 ext-idx) filename))) - -(defn- collect-and-persist-images - [svg-data file-id media-id] - (letfn [(process-image [{:keys [href] :as item}] - (try - (let [item (if (str/starts-with? href "data:") - (let [[mtype data] (parse-datauri href) - size (alength ^bytes data) - path (tmp/tempfile :prefix "penpot.media.download.") - written (io/write* path data :size size)] - - (when (not= written size) - (ex/raise :type :internal - :code :mismatch-write-size - :hint "unexpected state: unable to write to file")) - - (-> item - (assoc :size size) - (assoc :path path) - (assoc :filename "tempfile") - (assoc :mtype mtype))) - - (let [result (cmd.media/download-image *system* href)] - (-> (merge item result) - (assoc :name (extract-name href)))))] - - ;; The media processing adds the data to the - ;; input map and returns it. - (media/run {:cmd :info :input item})) - - (catch Throwable _ - (l/wrn :hint "unable to process embedded images on svg file" - :file-id (str file-id) - :media-id (str media-id)) - nil))) - - (persist-image [acc {:keys [path size width height mtype href] :as item}] - (let [storage (::sto/storage *system*) - conn (::db/conn *system*) - hash (sto/calculate-hash path) - content (-> (sto/content path size) - (sto/wrap-with-hash hash)) - params {::sto/content content - ::sto/deduplicate? true - ::sto/touched-at (:ts item) - :content-type mtype - :bucket "file-media-object"} - image (sto/put-object! storage params) - fmo-id (uuid/next)] - - (db/exec-one! conn - [cmd.media/sql:create-file-media-object - fmo-id - file-id true (:name item "image") - (:id image) - nil - width - height - mtype]) - - (assoc acc href {:id fmo-id - :mtype mtype - :width width - :height height})))] - - (let [images (->> (csvg/collect-images svg-data) - (transduce (keep process-image) - (completing persist-image) {}))] - (assoc svg-data :image-data images)))) - -(defn- resolve-sobject-id - [id] - (let [fmobject (db/get *system* :file-media-object {:id id} - {::sql/columns [:media-id]})] - (:media-id fmobject))) - -(defn get-sobject-content - [id] - (let [storage (::sto/storage *system*) - sobject (sto/get-object storage id)] - - (when-not sobject - (throw (RuntimeException. "sobject is nil"))) - (when (> (:size sobject) 1135899) - (throw (RuntimeException. "svg too big"))) - - (with-open [stream (sto/get-object-data storage sobject)] - (slurp stream)))) - -(defn get-optimized-svg - [sid] - (let [svg-text (get-sobject-content sid) - svg-text (if (contains? cf/flags :backend-svgo) - (svgo/optimize *system* svg-text) - svg-text)] - (csvg/parse svg-text))) - -(def base-path "/data/cache") - -(defn get-sobject-cache-path - [sid] - (let [path (impl/id->path sid)] - (fs/join base-path path))) - -(defn get-cached-svg - [sid] - (let [path (get-sobject-cache-path sid)] - (if (fs/exists? path) - (with-open [^java.lang.AutoCloseable stream (io/input-stream path)] - (let [reader (fres/reader stream)] - (fres/read! reader))) - (get-optimized-svg sid)))) - -(defn- create-shapes-for-svg - [{:keys [id] :as mobj} file-id objects frame-id position] - (let [sid (resolve-sobject-id id) - svg-data (if *cache* - (get-cached-svg sid) - (get-optimized-svg sid)) - svg-data (collect-and-persist-images svg-data file-id id) - svg-data (assoc svg-data :name (:name mobj))] - - (sbuilder/create-svg-shapes svg-data position objects frame-id frame-id #{} false))) - -(defn- process-media-object - [fdata page-id frame-id mobj position shape-cb] - (let [page (ctpl/get-page fdata page-id) - file-id (get fdata :id) - - [shape children] - (if (= (:mtype mobj) "image/svg+xml") - (create-shapes-for-svg mobj file-id (:objects page) frame-id position) - (create-shapes-for-bitmap mobj frame-id position)) - - shape (assoc shape :name (-> "Graphics" - (cfh/merge-path-item (:path mobj)) - (cfh/merge-path-item (:name mobj)))) - - changes - (-> (fcb/empty-changes nil) - (fcb/set-save-undo? false) - (fcb/with-page page) - (fcb/with-objects (:objects page)) - (fcb/with-library-data fdata) - (fcb/delete-media (:id mobj)) - (fcb/add-objects (cons shape children))) - - ;; NOTE: this is a workaround for `generate-add-component`, it - ;; is needed because that function always starts from empty - ;; changes; so in this case we need manually add all shapes to - ;; the page and then use that page for the - ;; `generate-add-component` function - page - (reduce (fn [page shape] - (ctst/add-shape (:id shape) - shape - page - frame-id - frame-id - nil - true)) - page - (cons shape children)) - - [_ _ changes] - (cll/generate-add-component changes - [shape] - (:objects page) - (:id page) - file-id - cfsh/prepare-create-artboard-from-selection)] - - (shape-cb shape) - (:redo-changes changes))) - -(defn- create-media-grid - [fdata page-id frame-id grid media-group shape-cb] - (letfn [(process [fdata mobj position] - (let [position (gpt/add position (gpt/point grid-gap grid-gap)) - tp (dt/tpoint) - err (volatile! false)] - (try - (let [changes (process-media-object fdata page-id frame-id mobj position shape-cb)] - (cp/process-changes fdata changes false)) - - (catch Throwable cause - (vreset! err true) - (let [cause (pu/unwrap-exception cause) - edata (ex-data cause)] - (cond - (instance? org.xml.sax.SAXParseException cause) - (l/inf :hint "skip processing media object: invalid svg found" - :file-id (str (:id fdata)) - :id (str (:id mobj))) - - (= (:type edata) :not-found) - (l/inf :hint "skip processing media object: underlying object does not exist" - :file-id (str (:id fdata)) - :id (str (:id mobj))) - - :else - (let [skip? *skip-on-graphic-error*] - (l/wrn :hint "unable to process file media object" - :skiped skip? - :file-id (str (:id fdata)) - :id (str (:id mobj)) - :cause cause) - (when-not skip? - (throw cause)))) - nil)) - (finally - (let [elapsed (tp)] - (l/trc :hint "graphic processed" - :file-id (str (:id fdata)) - :media-id (str (:id mobj)) - :error @err - :elapsed (dt/format-duration elapsed)))))))] - - (->> (d/zip media-group grid) - (reduce (fn [fdata [mobj position]] - (or (process fdata mobj position) fdata)) - (assoc-in fdata [:options :components-v2] true))))) - -(defn- fix-graphics-size - [fdata new-grid page-id frame-id] - (let [modify-shape (fn [page shape-id modifiers] - (ctn/update-shape page shape-id #(gsh/transform-shape % modifiers))) - - resize-frame (fn [page] - (let [{:keys [width height]} (meta new-grid) - - frame (ctst/get-shape page frame-id) - width (+ width grid-gap) - height (+ height grid-gap) - - modif-frame (ctm/resize nil - (gpt/point (/ width (:width frame)) - (/ height (:height frame))) - (gpt/point (:x frame) (:y frame)))] - - (modify-shape page frame-id modif-frame))) - - move-components (fn [page] - (let [frame (get (:objects page) frame-id) - shapes (map (d/getf (:objects page)) (:shapes frame))] - (->> (d/zip shapes new-grid) - (reduce (fn [page [shape position]] - (let [position (gpt/add position (gpt/point grid-gap grid-gap)) - modif-shape (ctm/move nil - (gpt/point (- (:x position) (:x (:selrect shape))) - (- (:y position) (:y (:selrect shape))))) - children-ids (cfh/get-children-ids-with-self (:objects page) (:id shape))] - (reduce #(modify-shape %1 %2 modif-shape) - page - children-ids))) - page))))] - (-> fdata - (ctpl/update-page page-id resize-frame) - (ctpl/update-page page-id move-components)))) - -(defn- migrate-graphics - [fdata] - (if (empty? (:media fdata)) - fdata - (let [[fdata page-id start-pos] - (ctf/get-or-add-library-page fdata frame-gap) - - media (->> (vals (:media fdata)) - (map (fn [{:keys [width height] :as media}] - (let [points (-> (grc/make-rect 0 0 width height) - (grc/rect->points))] - (assoc media :points points))))) - - groups (get-asset-groups media "Graphics")] - - (let [total (count media)] - (some-> *stats* (swap! update :processed-graphics (fnil + 0) total)) - (some-> *team-stats* (swap! update :processed-graphics (fnil + 0) total)) - (some-> *file-stats* (swap! assoc :processed-graphics total))) - - (loop [groups (seq groups) - fdata fdata - position start-pos] - (if (empty? groups) - fdata - (let [[group-name assets] (first groups) - grid (ctst/generate-shape-grid assets position grid-gap) - {:keys [width height]} (meta grid) - frame (create-frame group-name position width height) - fdata (ctpl/update-page fdata - page-id - #(ctst/add-shape (:id frame) - frame - % - (:id frame) - (:id frame) - nil - true)) - new-shapes (volatile! []) - add-shape #(vswap! new-shapes conj %) - - fdata' (create-media-grid fdata page-id (:id frame) grid assets add-shape) - - ;; When svgs had different width&height and viewport, - ;; sometimes the old graphics importer didn't - ;; calculate well the media object size. So, after - ;; migration we recalculate grid size from the actual - ;; size of the created shapes. - fdata' (if-let [grid (ctst/generate-shape-grid @new-shapes position grid-gap)] - (let [{new-width :width new-height :height} (meta grid)] - (if-not (and (mth/close? width new-width) (mth/close? height new-height)) - (do - (l/inf :hint "fixing graphics sizes" - :file-id (str (:id fdata)) - :group group-name) - (fix-graphics-size fdata' grid page-id (:id frame))) - fdata')) - fdata')] - - (recur (next groups) - fdata' - (gpt/add position (gpt/point 0 (+ height (* 2 grid-gap) frame-gap)))))))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; PRIVATE HELPERS -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- migrate-fdata - [fdata libs] - (let [migrated? (dm/get-in fdata [:options :components-v2])] - (if migrated? - fdata - (let [fdata (migrate-components fdata libs) - fdata (migrate-graphics fdata)] - (update fdata :options assoc :components-v2 true))))) - -;; FIXME: revisit this fn -(defn- fix-version* - [{:keys [version] :as file}] - (if (int? version) - file - (let [version (or (-> file :data :version) 0)] - (-> file - (assoc :version version) - (update :data dissoc :version))))) - -(defn- fix-version - [file] - (let [file (fix-version* file)] - (if (> (:version file) 22) - (assoc file :version 22) - file))) - -(defn- get-file - [system id] - (binding [pmap/*load-fn* (partial fdata/load-pointer system id)] - (-> (db/get system :file {:id id} - {::db/remove-deleted false - ::db/check-deleted false}) - (decode-row) - (update :data assoc :id id) - (update :data fdata/process-pointers deref) - (update :data fdata/process-objects (partial into {})) - (fix-version) - (fmg/migrate-file)))) - -(defn get-team - [system team-id] - (-> (db/get system :team {:id team-id} - {::db/remove-deleted false - ::db/check-deleted false}) - (update :features db/decode-pgarray #{}))) - -(defn- validate-file! - [file libs] - (cfv/validate-file! file libs) - (cfv/validate-file-schema! file)) - -(defn- persist-file! - [{:keys [::db/conn] :as system} {:keys [id] :as file}] - (let [file (if (contains? (:features file) "fdata/objects-map") - (fdata/enable-objects-map file) - file) - - file (if (contains? (:features file) "fdata/pointer-map") - (binding [pmap/*tracked* (pmap/create-tracked)] - (let [file (fdata/enable-pointer-map file)] - (fdata/persist-pointers! system id) - file)) - file) - - ;; Ensure all files has :data with id - file (update file :data assoc :id id)] - - (db/update! conn :file - {:data (blob/encode (:data file)) - :features (db/create-array conn "text" (:features file)) - :version (:version file) - :revn (:revn file)} - {:id (:id file)}))) - -(defn- process-file! - [{:keys [::db/conn] :as system} {:keys [id] :as file} & {:keys [validate?]}] - (let [libs (->> (files/get-file-libraries conn id) - (into [file] (comp (map :id) - (map (partial get-file system)))) - (d/index-by :id)) - - file (-> file - (update :data migrate-fdata libs) - (update :features conj "components/v2"))] - - (when validate? - (validate-file! file libs)) - - file)) - -(def ^:private sql:get-and-lock-team-files - "SELECT f.id - FROM file AS f - JOIN project AS p ON (p.id = f.project_id) - WHERE p.team_id = ? - AND p.deleted_at IS NULL - AND f.deleted_at IS NULL - FOR UPDATE") - -(defn get-and-lock-team-files - [conn team-id] - (->> (db/cursor conn [sql:get-and-lock-team-files team-id]) - (map :id))) - -(defn update-team! - [system {:keys [id] :as team}] - (let [conn (db/get-connection system) - params (-> team - (update :features db/encode-pgarray conn "text") - (dissoc :id))] - (db/update! conn :team - params - {:id id}) - team)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; PUBLIC API -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn migrate-file! - [system file-id & {:keys [validate? skip-on-graphic-error? label rown]}] - (let [tpoint (dt/tpoint) - err (volatile! false)] - - (binding [*file-stats* (atom {}) - *skip-on-graphic-error* skip-on-graphic-error?] - (try - (l/dbg :hint "migrate:file:start" - :file-id (str file-id) - :validate validate? - :skip-on-graphic-error skip-on-graphic-error?) - - (db/tx-run! system - (fn [system] - (binding [*system* system] - (when (string? label) - (fsnap/create-file-snapshot! system nil file-id (str "migration/" label))) - - (let [file (get-file system file-id) - file (process-file! system file :validate? validate?)] - - (persist-file! system file))))) - - (catch Throwable cause - (vreset! err true) - (l/wrn :hint "error on processing file" - :file-id (str file-id) - :cause cause) - (throw cause)) - - (finally - (let [elapsed (tpoint) - components (get @*file-stats* :processed-components 0) - graphics (get @*file-stats* :processed-graphics 0)] - - (l/dbg :hint "migrate:file:end" - :file-id (str file-id) - :graphics graphics - :components components - :validate validate? - :rown rown - :error @err - :elapsed (dt/format-duration elapsed)) - - (some-> *stats* (swap! update :processed-files (fnil inc 0))) - (some-> *team-stats* (swap! update :processed-files (fnil inc 0))))))))) - -(defn migrate-team! - [system team-id & {:keys [validate? rown skip-on-graphic-error? label]}] - - (l/dbg :hint "migrate:team:start" - :team-id (dm/str team-id) - :rown rown) - - (let [tpoint (dt/tpoint) - err (volatile! false) - - migrate-file - (fn [system file-id] - (migrate-file! system file-id - :label label - :validate? validate? - :skip-on-graphic-error? skip-on-graphic-error?)) - migrate-team - (fn [{:keys [::db/conn] :as system} team-id] - (let [{:keys [id features] :as team} (get-team system team-id)] - (if (contains? features "components/v2") - (l/inf :hint "team already migrated") - (let [features (-> features - (disj "ephimeral/v2-migration") - (conj "components/v2") - (conj "layout/grid") - (conj "styles/v2"))] - - (run! (partial migrate-file system) - (get-and-lock-team-files conn id)) - - (->> (assoc team :features features) - (update-team! conn))))))] - - (binding [*team-stats* (atom {})] - (try - (db/tx-run! system migrate-team team-id) - - (catch Throwable cause - (vreset! err true) - (l/wrn :hint "error on processing team" - :team-id (str team-id) - :cause cause) - (throw cause)) - - (finally - (let [elapsed (tpoint) - components (get @*team-stats* :processed-components 0) - graphics (get @*team-stats* :processed-graphics 0) - files (get @*team-stats* :processed-files 0)] - - (when-not @err - (some-> *stats* (swap! update :processed-teams (fnil inc 0)))) - - (l/dbg :hint "migrate:team:end" - :team-id (dm/str team-id) - :rown rown - :files files - :components components - :graphics graphics - :elapsed (dt/format-duration elapsed)))))))) diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index bd1c1e1b8..5df82a266 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -8,12 +8,11 @@ "Media & Font postprocessing." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.media :as cm] [app.common.schema :as sm] [app.common.schema.openapi :as-alias oapi] - [app.common.spec :as us] - [app.common.svg :as csvg] [app.config :as cf] [app.db :as-alias db] [app.storage :as-alias sto] @@ -22,39 +21,38 @@ [buddy.core.bytes :as bb] [buddy.core.codecs :as bc] [clojure.java.shell :as sh] - [clojure.spec.alpha :as s] + [clojure.xml :as xml] [cuerdas.core :as str] [datoteka.fs :as fs] [datoteka.io :as io]) (:import + clojure.lang.XMLHandler + java.io.InputStream + javax.xml.XMLConstants + javax.xml.parsers.SAXParserFactory + org.apache.commons.io.IOUtils org.im4java.core.ConvertCmd org.im4java.core.IMOperation org.im4java.core.Info)) -(s/def ::path fs/path?) -(s/def ::filename string?) -(s/def ::size integer?) -(s/def ::headers (s/map-of string? string?)) -(s/def ::mtype string?) +(def schema:upload + (sm/register! + ^{::sm/type ::upload} + [:map {:title "Upload"} + [:filename :string] + [:size ::sm/int] + [:path ::fs/path] + [:mtype {:optional true} :string] + [:headers {:optional true} + [:map-of :string :string]]])) -(s/def ::upload - (s/keys :req-un [::filename ::size ::path] - :opt-un [::mtype ::headers])) +(def ^:private schema:input + [:map {:title "Input"} + [:path ::fs/path] + [:mtype {:optional true} ::sm/text]]) -;; A subset of fields from the ::upload spec -(s/def ::input - (s/keys :req-un [::path] - :opt-un [::mtype])) - -(sm/register! - ^{::sm/type ::upload} - [:map {:title "Upload"} - [:filename :string] - [:size ::sm/int] - [:path ::fs/path] - [:mtype {:optional true} :string] - [:headers {:optional true} - [:map-of :string :string]]]) +(def ^:private check-input + (sm/check-fn schema:input)) (defn validate-media-type! ([upload] (validate-media-type! upload cm/valid-image-types)) @@ -97,17 +95,44 @@ (catch Throwable e (process-error e)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SVG PARSING +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- secure-parser-factory + [^InputStream input ^XMLHandler handler] + (.. (doto (SAXParserFactory/newInstance) + (.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true) + (.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true)) + (newSAXParser) + (parse input handler))) + +(defn- strip-doctype + [data] + (cond-> data + (str/includes? data "]*>" ""))) + +(defn- parse-svg + [text] + (let [text (strip-doctype text)] + (dm/with-open [istream (IOUtils/toInputStream text "UTF-8")] + (xml/parse istream secure-parser-factory)))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; IMAGE THUMBNAILS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::width integer?) -(s/def ::height integer?) -(s/def ::format #{:jpeg :webp :png}) -(s/def ::quality #(< 0 % 101)) +(def ^:private schema:thumbnail-params + [:map {:title "ThumbnailParams"} + [:input schema:input] + [:format [:enum :jpeg :webp :png]] + [:quality [:int {:min 1 :max 100}]] + [:width :int] + [:height :int]]) -(s/def ::thumbnail-params - (s/keys :req-un [::input ::format ::width ::height])) +(def ^:private check-thumbnail-params + (sm/check-fn schema:thumbnail-params)) ;; Related info on how thumbnails generation ;; http://www.imagemagick.org/Usage/thumbnails/ @@ -129,30 +154,38 @@ :data tmp))) (defmethod process :generic-thumbnail - [{:keys [quality width height] :as params}] - (us/assert ::thumbnail-params params) - (let [op (doto (IMOperation.) - (.addImage) - (.autoOrient) - (.strip) - (.thumbnail ^Integer (int width) ^Integer (int height) ">") - (.quality (double quality)) - (.addImage))] - (generic-process (assoc params :operation op)))) + [params] + (let [{:keys [quality width height] :as params} + (check-thumbnail-params params) + + operation + (doto (IMOperation.) + (.addImage) + (.autoOrient) + (.strip) + (.thumbnail ^Integer (int width) ^Integer (int height) ">") + (.quality (double quality)) + (.addImage))] + + (generic-process (assoc params :operation operation)))) (defmethod process :profile-thumbnail - [{:keys [quality width height] :as params}] - (us/assert ::thumbnail-params params) - (let [op (doto (IMOperation.) - (.addImage) - (.autoOrient) - (.strip) - (.thumbnail ^Integer (int width) ^Integer (int height) "^") - (.gravity "center") - (.extent (int width) (int height)) - (.quality (double quality)) - (.addImage))] - (generic-process (assoc params :operation op)))) + [params] + (let [{:keys [quality width height] :as params} + (check-thumbnail-params params) + + operation + (doto (IMOperation.) + (.addImage) + (.autoOrient) + (.strip) + (.thumbnail ^Integer (int width) ^Integer (int height) "^") + (.gravity "center") + (.extent (int width) (int height)) + (.quality (double quality)) + (.addImage))] + + (generic-process (assoc params :operation operation)))) (defn get-basic-info-from-svg [{:keys [tag attrs] :as data}] @@ -184,10 +217,9 @@ (defmethod process :info [{:keys [input] :as params}] - (us/assert ::input input) - (let [{:keys [path mtype]} input] + (let [{:keys [path mtype] :as input} (check-input input)] (if (= mtype "image/svg+xml") - (let [info (some-> path slurp csvg/parse get-basic-info-from-svg)] + (let [info (some-> path slurp parse-svg get-basic-info-from-svg)] (when-not info (ex/raise :type :validation :code :invalid-svg-file diff --git a/backend/src/app/rpc/climit.clj b/backend/src/app/rpc/climit.clj index bb3db5ba5..542988bf5 100644 --- a/backend/src/app/rpc/climit.clj +++ b/backend/src/app/rpc/climit.clj @@ -22,7 +22,8 @@ [datoteka.fs :as fs] [integrant.core :as ig] [promesa.exec :as px] - [promesa.exec.bulkhead :as pbh]) + [promesa.exec.bulkhead :as pbh] + [promesa.protocols :as pt]) (:import clojure.lang.ExceptionInfo java.util.concurrent.atomic.AtomicLong)) @@ -178,12 +179,13 @@ (measure metrics mlabels stats nil) (log "enqueued" req-id stats limit-id limit-label limit-params nil)) - (px/invoke! limiter (fn [] - (let [elapsed (tpoint) - stats (pbh/get-stats limiter)] - (measure metrics mlabels stats elapsed) - (log "acquired" req-id stats limit-id limit-label limit-params elapsed) - (handler)))) + ;; WORKAROUND: this is a temporal change until the bug is fixed in funcool/promesa + @(pt/-submit! limiter (fn [] + (let [elapsed (tpoint) + stats (pbh/get-stats limiter)] + (measure metrics mlabels stats elapsed) + (log "acquired" req-id stats limit-id limit-label limit-params elapsed) + (handler)))) (catch ExceptionInfo cause (let [{:keys [type code]} (ex-data cause)] diff --git a/backend/src/app/rpc/commands/files_temp.clj b/backend/src/app/rpc/commands/files_temp.clj index f639bebdb..f1e603f56 100644 --- a/backend/src/app/rpc/commands/files_temp.clj +++ b/backend/src/app/rpc/commands/files_temp.clj @@ -14,7 +14,6 @@ [app.config :as cf] [app.db :as db] [app.db.sql :as sql] - [app.features.components-v2 :as feat.compv2] [app.features.fdata :as fdata] [app.loggers.audit :as audit] [app.rpc :as-alias rpc] @@ -110,7 +109,7 @@ ;; --- MUTATION COMMAND: persist-temp-file (defn persist-temp-file - [{:keys [::db/conn] :as cfg} {:keys [id ::rpc/profile-id] :as params}] + [{:keys [::db/conn] :as cfg} {:keys [id] :as params}] (let [file (files/get-file cfg id :migrate? false :lock-for-update? true)] @@ -119,7 +118,6 @@ (ex/raise :type :validation :code :cant-persist-already-persisted-file)) - (let [changes (->> (db/cursor conn (sql/select :file-change {:file-id id} {:order-by [[:revn :asc]]}) @@ -147,19 +145,6 @@ :revn 1 :data (blob/encode (:data file))} {:id id}) - - (let [team (teams/get-team conn :profile-id profile-id :project-id (:project-id file)) - file-features (:features file) - team-features (cfeat/get-team-enabled-features cf/flags team)] - (when (and (contains? team-features "components/v2") - (not (contains? file-features "components/v2"))) - ;; Migrate components v2 - (feat.compv2/migrate-file! cfg - (:id file) - :max-procs 2 - :validate? true - :throw-on-validate? true))) - nil))) (def ^:private schema:persist-temp-file diff --git a/backend/src/app/srepl/components_v2.clj b/backend/src/app/srepl/components_v2.clj deleted file mode 100644 index 27a8d9825..000000000 --- a/backend/src/app/srepl/components_v2.clj +++ /dev/null @@ -1,306 +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 INC - -(ns app.srepl.components-v2 - (:require - [app.common.fressian :as fres] - [app.common.logging :as l] - [app.db :as db] - [app.features.components-v2 :as feat] - [app.main :as main] - [app.srepl.helpers :as h] - [app.util.events :as events] - [app.util.time :as dt] - [app.worker :as-alias wrk] - [datoteka.fs :as fs] - [datoteka.io :as io] - [promesa.exec :as px] - [promesa.exec.semaphore :as ps] - [promesa.util :as pu])) - -(def ^:dynamic *scope* nil) -(def ^:dynamic *semaphore* nil) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; PRIVATE HELPERS -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(def ^:private sql:get-files-by-created-at - "SELECT id, features, - row_number() OVER (ORDER BY created_at DESC) AS rown - FROM file - WHERE deleted_at IS NULL - ORDER BY created_at DESC") - -(defn- get-files - [conn] - (->> (db/cursor conn [sql:get-files-by-created-at] {:chunk-size 500}) - (map feat/decode-row) - (remove (fn [{:keys [features]}] - (contains? features "components/v2"))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; PUBLIC API -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn migrate-file! - [file-id & {:keys [rollback? validate? label cache skip-on-graphic-error?] - :or {rollback? true - validate? false - skip-on-graphic-error? true}}] - (l/dbg :hint "migrate:start" :rollback rollback?) - (let [tpoint (dt/tpoint) - file-id (h/parse-uuid file-id)] - - (binding [feat/*stats* (atom {}) - feat/*cache* cache] - (try - (-> (assoc main/system ::db/rollback rollback?) - (feat/migrate-file! file-id - :validate? validate? - :skip-on-graphic-error? skip-on-graphic-error? - :label label)) - - (-> (deref feat/*stats*) - (assoc :elapsed (dt/format-duration (tpoint)))) - - (catch Throwable cause - (l/wrn :hint "migrate:error" :cause cause)) - - (finally - (let [elapsed (dt/format-duration (tpoint))] - (l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed))))))) - -(defn migrate-team! - [team-id & {:keys [rollback? skip-on-graphic-error? validate? label cache] - :or {rollback? true - validate? true - skip-on-graphic-error? true}}] - - (l/dbg :hint "migrate:start" :rollback rollback?) - - (let [team-id (h/parse-uuid team-id) - stats (atom {}) - tpoint (dt/tpoint)] - - (binding [feat/*stats* stats - feat/*cache* cache] - (try - (-> (assoc main/system ::db/rollback rollback?) - (feat/migrate-team! team-id - :label label - :validate? validate? - :skip-on-graphics-error? skip-on-graphic-error?)) - - (-> (deref feat/*stats*) - (assoc :elapsed (dt/format-duration (tpoint)))) - - (catch Throwable cause - (l/dbg :hint "migrate:error" :cause cause)) - - (finally - (let [elapsed (dt/format-duration (tpoint))] - (l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed))))))) - -(defn migrate-files! - "A REPL helper for migrate all files. - - This function starts multiple concurrent file migration processes - until thw maximum number of jobs is reached which by default has the - value of `1`. This is controled with the `:max-jobs` option. - - If you want to run this on multiple machines you will need to specify - the total number of partitions and the current partition. - - In order to get the report table populated, you will need to provide - a correct `:label`. That label is also used for persist a file - snaphot before continue with the migration." - [& {:keys [max-jobs max-items rollback? validate? - cache skip-on-graphic-error? - label partitions current-partition] - :or {validate? false - rollback? true - max-jobs 1 - current-partition 1 - skip-on-graphic-error? true - max-items Long/MAX_VALUE}}] - - (when (int? partitions) - (when-not (int? current-partition) - (throw (IllegalArgumentException. "missing `current-partition` parameter"))) - (when-not (<= 0 current-partition partitions) - (throw (IllegalArgumentException. "invalid value on `current-partition` parameter")))) - - (let [stats (atom {}) - tpoint (dt/tpoint) - factory (px/thread-factory :virtual false :prefix "penpot/migration/") - executor (px/cached-executor :factory factory) - - sjobs (ps/create :permits max-jobs) - - migrate-file - (fn [file-id rown] - (try - (db/tx-run! (assoc main/system ::db/rollback rollback?) - (fn [system] - (db/exec-one! system ["SET LOCAL idle_in_transaction_session_timeout = 0"]) - (feat/migrate-file! system file-id - :rown rown - :label label - :validate? validate? - :skip-on-graphic-error? skip-on-graphic-error?))) - - (catch Throwable cause - (l/wrn :hint "unexpected error on processing file (skiping)" - :file-id (str file-id)) - - (events/tap :error - (ex-info "unexpected error on processing file (skiping)" - {:file-id file-id} - cause)) - - (swap! stats update :errors (fnil inc 0))) - - (finally - (ps/release! sjobs)))) - - process-file - (fn [{:keys [id rown]}] - (ps/acquire! sjobs) - (px/run! executor (partial migrate-file id rown)))] - - (l/dbg :hint "migrate:start" - :label label - :rollback rollback? - :max-jobs max-jobs - :max-items max-items) - - (binding [feat/*stats* stats - feat/*cache* cache] - (try - (db/tx-run! main/system - (fn [{:keys [::db/conn] :as system}] - (db/exec! conn ["SET LOCAL statement_timeout = 0"]) - (db/exec! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"]) - - (run! process-file - (->> (get-files conn) - (filter (fn [{:keys [rown] :as row}] - (if (int? partitions) - (= current-partition (inc (mod rown partitions))) - true))) - (take max-items))) - - ;; Close and await tasks - (pu/close! executor))) - - (-> (deref stats) - (assoc :elapsed (dt/format-duration (tpoint)))) - - (catch Throwable cause - (l/dbg :hint "migrate:error" :cause cause) - (events/tap :error cause)) - - (finally - (let [elapsed (dt/format-duration (tpoint))] - (l/dbg :hint "migrate:end" - :rollback rollback? - :elapsed elapsed))))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; CACHE POPULATE -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(def sql:sobjects-for-cache - "SELECT id, - row_number() OVER (ORDER BY created_at) AS index - FROM storage_object - WHERE (metadata->>'~:bucket' = 'file-media-object' OR - metadata->>'~:bucket' IS NULL) - AND metadata->>'~:content-type' = 'image/svg+xml' - AND deleted_at IS NULL - AND size < 1135899 - ORDER BY created_at ASC") - -(defn populate-cache! - "A REPL helper for migrate all files. - - This function starts multiple concurrent file migration processes - until thw maximum number of jobs is reached which by default has the - value of `1`. This is controled with the `:max-jobs` option. - - If you want to run this on multiple machines you will need to specify - the total number of partitions and the current partition. - - In order to get the report table populated, you will need to provide - a correct `:label`. That label is also used for persist a file - snaphot before continue with the migration." - [& {:keys [max-jobs] :or {max-jobs 1}}] - - (let [tpoint (dt/tpoint) - - factory (px/thread-factory :virtual false :prefix "penpot/cache/") - executor (px/cached-executor :factory factory) - - sjobs (ps/create :permits max-jobs) - - retrieve-sobject - (fn [id index] - (let [path (feat/get-sobject-cache-path id) - parent (fs/parent path)] - - (try - (when-not (fs/exists? parent) - (fs/create-dir parent)) - - (if (fs/exists? path) - (l/inf :hint "create cache entry" :status "exists" :index index :id (str id) :path (str path)) - (let [svg-data (feat/get-optimized-svg id)] - (with-open [^java.lang.AutoCloseable stream (io/output-stream path)] - (let [writer (fres/writer stream)] - (fres/write! writer svg-data))) - - (l/inf :hint "create cache entry" :status "created" - :index index - :id (str id) - :path (str path)))) - - (catch Throwable cause - (l/wrn :hint "create cache entry" - :status "error" - :index index - :id (str id) - :path (str path) - :cause cause)) - - (finally - (ps/release! sjobs))))) - - process-sobject - (fn [{:keys [id index]}] - (ps/acquire! sjobs) - (px/run! executor (partial retrieve-sobject id index)))] - - (l/dbg :hint "migrate:start" - :max-jobs max-jobs) - - (try - (binding [feat/*system* main/system] - (run! process-sobject - (db/exec! main/system [sql:sobjects-for-cache])) - - ;; Close and await tasks - (pu/close! executor)) - - {:elapsed (dt/format-duration (tpoint))} - - (catch Throwable cause - (l/dbg :hint "populate:error" :cause cause)) - - (finally - (let [elapsed (dt/format-duration (tpoint))] - (l/dbg :hint "populate:end" - :elapsed elapsed)))))) diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index aee2ffd3d..2ce9e58f3 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -13,7 +13,6 @@ [app.common.files.migrations :as fmg] [app.common.files.validate :as cfv] [app.db :as db] - [app.features.components-v2 :as feat.comp-v2] [app.main :as main] [app.rpc.commands.files :as files] [app.rpc.commands.files-snapshot :as fsnap] @@ -62,6 +61,27 @@ {:id id}) team)) +(def ^:private sql:get-and-lock-team-files + "SELECT f.id + FROM file AS f + JOIN project AS p ON (p.id = f.project_id) + WHERE p.team_id = ? + AND p.deleted_at IS NULL + AND f.deleted_at IS NULL + FOR UPDATE") + +(defn get-team + [conn team-id] + (-> (db/get conn :team {:id team-id} + {::db/remove-deleted false + ::db/check-deleted false}) + (update :features db/decode-pgarray #{}))) + +(defn get-and-lock-team-files + [conn team-id] + (transduce (map :id) conj [] + (db/plan conn [sql:get-and-lock-team-files team-id]))) + (defn reset-file-data! "Hardcode replace of the data of one file." [system id data] @@ -96,7 +116,7 @@ (defn take-team-snapshot! [system team-id label] (let [conn (db/get-connection system)] - (->> (feat.comp-v2/get-and-lock-team-files conn team-id) + (->> (get-and-lock-team-files conn team-id) (reduce (fn [result file-id] (let [file (fsnap/get-file-snapshots system file-id)] (fsnap/create-file-snapshot! system file @@ -108,19 +128,16 @@ (defn restore-team-snapshot! [system team-id label] (let [conn (db/get-connection system) - ids (->> (feat.comp-v2/get-and-lock-team-files conn team-id) + ids (->> (get-and-lock-team-files conn team-id) (into #{})) snap (search-file-snapshots conn ids label) - ids' (into #{} (map :file-id) snap) - team (-> (feat.comp-v2/get-team conn team-id) - (update :features disj "components/v2"))] + ids' (into #{} (map :file-id) snap)] (when (not= ids ids') (throw (RuntimeException. "no uniform snapshot available"))) - (feat.comp-v2/update-team! conn team) (reduce (fn [result {:keys [file-id id]}] (fsnap/restore-file-snapshot! system file-id id) (inc result)) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index da4b45eda..a9ebb1379 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -22,7 +22,6 @@ [app.config :as cf] [app.db :as db] [app.db.sql :as-alias sql] - [app.features.components-v2 :as feat.comp-v2] [app.features.fdata :as feat.fdata] [app.loggers.audit :as audit] [app.main :as main] @@ -439,7 +438,7 @@ (binding [h/*system* system db/*conn* (db/get-connection system)] - (->> (feat.comp-v2/get-and-lock-team-files conn team-id) + (->> (h/get-and-lock-team-files conn team-id) (reduce (fn [result file-id] (if (h/process-file! system file-id update-fn opts) (inc result) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index da754ef96..951ee96a8 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/deps.edn b/common/deps.edn index 181f382df..c96648ac3 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -30,7 +30,7 @@ funcool/tubax {:mvn/version "2021.05.20-0"} funcool/cuerdas {:mvn/version "2023.11.09-407"} funcool/promesa - {:git/sha "0c5ed6ad033515a2df4b55addea044f60e9653d0" + {:git/sha "6c14b06d9d64fae6e43c1463ce313f2fdc0d989b" :git/url "https://github.com/funcool/promesa"} funcool/datoteka @@ -76,7 +76,8 @@ :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}} :shadow-cljs - {:main-opts ["-m" "shadow.cljs.devtools.cli"]} + {:main-opts ["-m" "shadow.cljs.devtools.cli"] + :jvm-opts ["--sun-misc-unsafe-memory-access=allow"]} :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}} diff --git a/common/package.json b/common/package.json index 2560d6822..46338a335 100644 --- a/common/package.json +++ b/common/package.json @@ -11,8 +11,7 @@ "url": "https://github.com/penpot/penpot" }, "dependencies": { - "luxon": "^3.4.4", - "sax": "^1.4.1" + "luxon": "^3.4.4" }, "devDependencies": { "concurrently": "^9.0.1", diff --git a/common/src/app/common/files/builder.cljc b/common/src/app/common/files/builder.cljc index c62b098dd..19e668193 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,17 +117,32 @@ (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] + [:frame-id {:optional true} ::sm/uuid] + [:page-id {:optional true} ::sm/uuid]]) + +(def ^:private check-add-component + (sm/check-fn schema:add-component + :hint "invalid arguments passed for 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] + [:frame-id {:optional true} ::sm/uuid] + [:page-id {:optional true} ::sm/uuid]]) -(def check-add-component-instance - (sm/check-fn schema:add-component-instance)) +(def ^:private check-add-component-instance + (sm/check-fn schema:add-component-instance + :hint "invalid arguments passed for add-component-instance")) (def decode-add-component-instance (sm/decode-fn schema:add-component-instance sm/json-transformer)) @@ -158,37 +155,77 @@ (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) + (update :id default-uuid)) + 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 +240,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 +337,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 +354,110 @@ {: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 page-id frame-id name path]} + (-> (check-add-component params) + (update :component-id default-uuid)) - 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) + file-id + (or file-id (::current-file-id state)) page-id - (get file ::current-page-id)] + (or 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") + frame-id + (or frame-id (get state ::current-frame-id)) - ;; 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 + :page-id page-id + :operations + [{:type :set :attr :component-root :val true} + {:type :set :attr :main-instance :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 frame-id page-id]} + (check-add-component-instance params) + + file-id + (or file-id (get state ::current-file-id)) + + frame-id + (or frame-id (get state ::current-frame-id)) + + page-id + (or page-id (get state ::current-page-id)) + + change + {:type :mod-obj + :id frame-id + :page-id page-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}]}] + + (commit-change state change))) (defn delete-shape [file id] @@ -423,10 +469,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 +488,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 +497,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 + (update :id default-uuid)) + page-id (::current-page-id state)] + (-> state (commit-change {:type :set-guide :page-id page-id @@ -463,24 +511,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 23dda4ac1..47409130f 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 a342a227f..b6f2311fb 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/svg.cljc b/common/src/app/common/svg.cljc index c542f2222..a3b309b14 100644 --- a/common/src/app/common/svg.cljc +++ b/common/src/app/common/svg.cljc @@ -6,8 +6,6 @@ (ns app.common.svg (:require - #?(:clj [clojure.xml :as xml] - :cljs [tubax.core :as tubax]) [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] @@ -15,15 +13,7 @@ [app.common.geom.shapes :as gsh] [app.common.math :as mth] [app.common.uuid :as uuid] - [cuerdas.core :as str]) - #?(:clj - (:import - clojure.lang.XMLHandler - java.io.InputStream - javax.xml.XMLConstants - javax.xml.parsers.SAXParserFactory - org.apache.commons.io.IOUtils))) - + [cuerdas.core :as str])) ;; Regex for XML ids per Spec ;; https://www.w3.org/TR/2008/REC-xml-20081126/#sec-common-syn @@ -1030,24 +1020,3 @@ :height (d/parse-integer (:height attrs) 0)})))] (reduce-nodes redfn [] svg-data))) -#?(:clj - (defn- secure-parser-factory - [^InputStream input ^XMLHandler handler] - (.. (doto (SAXParserFactory/newInstance) - (.setFeature XMLConstants/FEATURE_SECURE_PROCESSING true) - (.setFeature "http://apache.org/xml/features/disallow-doctype-decl" true)) - (newSAXParser) - (parse input handler)))) - -(defn strip-doctype - [data] - (cond-> data - (str/includes? data "]*>" ""))) - -(defn parse - [text] - #?(:cljs (tubax/xml->clj text) - :clj (let [text (strip-doctype text)] - (dm/with-open [istream (IOUtils/toInputStream text "UTF-8")] - (xml/parse istream secure-parser-factory))))) diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index eb2018fad..9fda10d36 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 e4ee9ceb9..ca3a3e94f 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/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 98cd2d302..edf01e4b1 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -1506,12 +1506,13 @@ Will return a value that matches this schema: (-> (make-tokens-lib) (decode-dtcg-json encoded-json))) -(def type:tokens-lib - {:type ::tokens-lib - :pred valid-tokens-lib? - :type-properties - {:encode/json encode-dtcg - :decode/json decode-dtcg}}) +(def schema:tokens-lib + (sm/register! + {:type ::tokens-lib + :pred valid-tokens-lib? + :type-properties + {:encode/json encode-dtcg + :decode/json decode-dtcg}})) (defn duplicate-set [set-name lib & {:keys [suffix]}] (let [sets (get-sets lib) @@ -1521,8 +1522,6 @@ Will return a value that matches this schema: (assoc :name copy-name) (assoc :modified-at (dt/now))))) -(sm/register! type:tokens-lib) - ;; === Serialization handlers for RPC API (transit) and database (fressian) (t/add-handlers! diff --git a/common/test/common_tests/files_builder_test.cljc b/common/test/common_tests/files_builder_test.cljc deleted file mode 100644 index d8a4d9dd6..000000000 --- a/common/test/common_tests/files_builder_test.cljc +++ /dev/null @@ -1,26 +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 INC - -(ns common-tests.files-builder-test - (:require - [app.common.files.builder :as builder] - [clojure.test :as t])) - -(t/deftest test-strip-image-extension - (t/testing "removes extension from supported image files" - (t/is (= (builder/strip-image-extension "foo.png") "foo")) - (t/is (= (builder/strip-image-extension "foo.webp") "foo")) - (t/is (= (builder/strip-image-extension "foo.jpg") "foo")) - (t/is (= (builder/strip-image-extension "foo.jpeg") "foo")) - (t/is (= (builder/strip-image-extension "foo.svg") "foo")) - (t/is (= (builder/strip-image-extension "foo.gif") "foo"))) - - (t/testing "does not remove extension for unsupported files" - (t/is (= (builder/strip-image-extension "foo.txt") "foo.txt")) - (t/is (= (builder/strip-image-extension "foo.bmp") "foo.bmp"))) - - (t/testing "leaves filename intact when it has no extension" - (t/is (= (builder/strip-image-extension "README") "README")))) diff --git a/common/test/common_tests/media_test.cljc b/common/test/common_tests/media_test.cljc new file mode 100644 index 000000000..5098bf6e8 --- /dev/null +++ b/common/test/common_tests/media_test.cljc @@ -0,0 +1,26 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns common-tests.media-test + (:require + [app.common.media :as media] + [clojure.test :as t])) + +(t/deftest test-strip-image-extension + (t/testing "removes extension from supported image files" + (t/is (= (media/strip-image-extension "foo.png") "foo")) + (t/is (= (media/strip-image-extension "foo.webp") "foo")) + (t/is (= (media/strip-image-extension "foo.jpg") "foo")) + (t/is (= (media/strip-image-extension "foo.jpeg") "foo")) + (t/is (= (media/strip-image-extension "foo.svg") "foo")) + (t/is (= (media/strip-image-extension "foo.gif") "foo"))) + + (t/testing "does not remove extension for unsupported files" + (t/is (= (media/strip-image-extension "foo.txt") "foo.txt")) + (t/is (= (media/strip-image-extension "foo.bmp") "foo.bmp"))) + + (t/testing "leaves filename intact when it has no extension" + (t/is (= (media/strip-image-extension "README") "README")))) diff --git a/common/test/common_tests/runner.cljc b/common/test/common_tests/runner.cljc index 09c25061e..287f6c42d 100644 --- a/common/test/common_tests/runner.cljc +++ b/common/test/common_tests/runner.cljc @@ -9,7 +9,6 @@ [clojure.test :as t] [common-tests.colors-test] [common-tests.data-test] - [common-tests.files-builder-test] [common-tests.files-changes-test] [common-tests.files-migrations-test] [common-tests.geom-point-test] @@ -29,6 +28,7 @@ [common-tests.logic.swap-and-reset-test] [common-tests.logic.swap-as-override-test] [common-tests.logic.token-test] + [common-tests.media-test] [common-tests.pages-helpers-test] [common-tests.record-test] [common-tests.schema-test] @@ -58,7 +58,6 @@ (t/run-tests 'common-tests.colors-test 'common-tests.data-test - 'common-tests.files-builder-test 'common-tests.files-changes-test 'common-tests.files-migrations-test 'common-tests.geom-point-test @@ -78,6 +77,7 @@ 'common-tests.logic.swap-and-reset-test 'common-tests.logic.swap-as-override-test 'common-tests.logic.token-test + 'common-tests.media-test 'common-tests.pages-helpers-test 'common-tests.record-test 'common-tests.schema-test @@ -85,11 +85,11 @@ 'common-tests.svg-test 'common-tests.text-test 'common-tests.time-test - 'common-tests.types.modifiers-test - 'common-tests.types.shape-interactions-test - 'common-tests.types.shape-decode-encode-test - 'common-tests.types.tokens-lib-test - 'common-tests.types.components-test 'common-tests.types.absorb-assets-test + 'common-tests.types.components-test + 'common-tests.types.modifiers-test 'common-tests.types.path-data-test + 'common-tests.types.shape-decode-encode-test + 'common-tests.types.shape-interactions-test + 'common-tests.types.tokens-lib-test 'common-tests.uuid-test)) diff --git a/common/vendor/beicon/impl/rxjs.cljs b/common/vendor/beicon/impl/rxjs.cljs deleted file mode 100644 index 6fa0bcfa0..000000000 --- a/common/vendor/beicon/impl/rxjs.cljs +++ /dev/null @@ -1,4 +0,0 @@ -(ns beicon.impl.rxjs - (:require ["rxjs" :as rx])) - -(goog/exportSymbol "rxjsMain" rx) diff --git a/common/vendor/beicon/impl/rxjs_operators.cljs b/common/vendor/beicon/impl/rxjs_operators.cljs deleted file mode 100644 index 22b4e2313..000000000 --- a/common/vendor/beicon/impl/rxjs_operators.cljs +++ /dev/null @@ -1,4 +0,0 @@ -(ns beicon.impl.rxjs-operators - (:require ["rxjs/operators" :as rxop])) - -(goog/exportSymbol "rxjsOperators" rxop) diff --git a/common/vendor/tubax/saxjs.cljs b/common/vendor/tubax/saxjs.cljs deleted file mode 100644 index 3dc98550b..000000000 --- a/common/vendor/tubax/saxjs.cljs +++ /dev/null @@ -1,4 +0,0 @@ -(ns tubax.saxjs - (:require ["sax" :as sax])) - -(goog/exportSymbol "sax" sax) diff --git a/common/yarn.lock b/common/yarn.lock index d130bd137..d323d4bf9 100644 --- a/common/yarn.lock +++ b/common/yarn.lock @@ -260,7 +260,6 @@ __metadata: concurrently: "npm:^9.0.1" luxon: "npm:^3.4.4" nodemon: "npm:^3.1.7" - sax: "npm:^1.4.1" shadow-cljs: "npm:3.0.5" source-map-support: "npm:^0.5.21" ws: "npm:^8.17.0" @@ -981,13 +980,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": version: 7.6.2 resolution: "semver@npm:7.6.2" diff --git a/frontend/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch b/frontend/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch new file mode 100644 index 000000000..2a413b389 --- /dev/null +++ b/frontend/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch @@ -0,0 +1,18 @@ +diff --git a/lib/zip-fs.js b/lib/zip-fs.js +index 1444c0f00e5f1ad6c13521f90a7f3c6659d81116..90e38baef5365c2abbcb9337f7ab37f800e883a4 100644 +--- a/lib/zip-fs.js ++++ b/lib/zip-fs.js +@@ -33,12 +33,7 @@ import { initShimAsyncCodec } from "./core/util/stream-codec-shim.js"; + import { terminateWorkers } from "./core/codec-pool.js"; + + let baseURL; +-try { +- baseURL = import.meta.url; +- // eslint-disable-next-line no-unused-vars +-} catch (_) { +- // ignored +-} ++ + configure({ baseURL }); + configureWebWorker(configure); + diff --git a/frontend/package.json b/frontend/package.json index 6bd8f77a0..baf261b20 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "url": "https://github.com/penpot/penpot" }, "resolutions": { + "@zip.js/zip.js@npm:^2.7.44": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch", "@vitejs/plugin-react": "^4.2.0" }, "scripts": { @@ -25,7 +26,6 @@ "build:app:libs": "node ./scripts/build-libs.js", "build:app:main": "clojure -M:dev:shadow-cljs release main worker", "build:app": "yarn run clear:shadow-cache && yarn run build:app:main && yarn run build:app:libs", - "build:library": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs release library", "e2e:server": "node ./scripts/e2e-server.js", "fmt:clj": "cljfmt fix --parallel=true src/ test/", "fmt:clj:check": "cljfmt check --parallel=false src/ test/", @@ -45,7 +45,6 @@ "watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook", "clear:shadow-cache": "rm -rf .shadow-cljs", "watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"", - "watch:library": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library", "watch": "yarn run watch:app:assets", "watch:storybook": "concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"", "watch:storybook:assets": "node ./scripts/watch-storybook.js" @@ -108,11 +107,11 @@ "@penpot/svgo": "penpot/svgo#v3.1", "@penpot/text-editor": "portal:./text-editor", "@tokens-studio/sd-transforms": "1.2.11", + "@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", "compression": "^1.7.5", "date-fns": "^4.1.0", "eventsource-parser": "^3.0.1", "js-beautify": "^1.15.4", - "jszip": "^3.10.1", "lodash": "^4.17.21", "lodash.debounce": "^4.0.8", "luxon": "^3.6.1", diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index 2cd8a48b8..85cced3ae 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -149,32 +149,6 @@ {:test {:init-fn frontend-tests.runner/init :prepend-js ";if (typeof globalThis.navigator?.userAgent === 'undefined') { globalThis.navigator = {userAgent: ''}; };"}}} - :library - {:target :esm - :runtime :custom - :output-dir "target/library" - :devtools {:autoload false} - - :modules - {:penpot - {:exports {BuilderError lib.file-builder/BuilderError - createFile lib.file-builder/create-file}}} - - :compiler-options - {:output-feature-set :es2020 - :output-wrapper false - :warnings {:fn-deprecated false}} - - :release - {:compiler-options - {:fn-invoke-direct true - :optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced] - :pretty-print false - :source-map true - :elide-asserts true - :anon-fn-naming-policy :off - :source-map-detail-level :all}}} - :bench {:target :node-script :output-to "target/bench.js" diff --git a/frontend/src/app/main/data/exports/files.cljs b/frontend/src/app/main/data/exports/files.cljs index b89c027fe..74001f1f9 100644 --- a/frontend/src/app/main/data/exports/files.cljs +++ b/frontend/src/app/main/data/exports/files.cljs @@ -8,7 +8,6 @@ "The file exportation API and events" (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.schema :as sm] [app.main.data.event :as ev] [app.main.data.modal :as modal] @@ -35,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 67c9d1871..92a5f81a4 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 60c4ff25b..57b3d491e 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/src/app/main/ui/exports/files.cljs b/frontend/src/app/main/ui/exports/files.cljs index 529041dc4..92545be00 100644 --- a/frontend/src/app/main/ui/exports/files.cljs +++ b/frontend/src/app/main/ui/exports/files.cljs @@ -67,11 +67,11 @@ [:div {:class (stl/css :file-name-label)} (:name file)]]]) -(mf/defc export-dialog* +(mf/defc export-dialog {::mf/register modal/components ::mf/register-as ::fexp/export-files ::mf/props :obj} - [{:keys [team-id files features format]}] + [{:keys [team-id files format]}] (let [state* (mf/use-state (partial initialize-state files)) has-libs? (some :has-libraries files) @@ -88,14 +88,13 @@ start-export (mf/use-fn - (mf/deps team-id selected files features) + (mf/deps team-id selected files) (fn [] (swap! state* assoc :status :exporting) (->> (mw/ask-many! {:cmd :export-files :format format :team-id team-id - :features features :type selected :files files}) (rx/mapcat #(->> (rx/of %) diff --git a/frontend/src/app/util/zip.cljs b/frontend/src/app/util/zip.cljs index a411e8343..0ddf6d90b 100644 --- a/frontend/src/app/util/zip.cljs +++ b/frontend/src/app/util/zip.cljs @@ -5,72 +5,82 @@ ;; Copyright (c) KALEIDOS INC (ns app.util.zip - "Helpers for make zip file (using jszip)." + "Helpers for make zip file." (:require - ["jszip" :as zip] - [app.util.http :as http] - [beicon.v2.core :as rx] + ["@zip.js/zip.js" :as zip] + [app.util.array :as array] [promesa.core :as p])) -(defn compress-files - [files] - (letfn [(attach-file [zobj [name content]] - (.file zobj name content))] - (let [zobj (zip.)] - (run! (partial attach-file zobj) files) - (->> (.generateAsync zobj #js {:type "blob"}) - (rx/from))))) - -(defn load-from-url - "Loads the data from a blob url" - [url] - (->> (http/send! - {:uri url - :response-type :blob - :method :get}) - (rx/map :body) - (rx/merge-map zip/loadAsync))) - -(defn- process-file - [entry path type] - ;; (js/console.log "zip:process-file" entry path type) +(defn reader + [blob] (cond - (nil? entry) - (p/rejected (str "File not found: " path)) + (instance? js/Blob blob) + (let [breader (new zip/BlobReader blob)] + (new zip/ZipReader breader)) - (.-dir ^js entry) - (p/resolved {:dir path}) + (instance? js/Uint8Array blob) + (let [breader (new zip/Uint8ArrayReader blob) + zreader (new zip/ZipReader breader #js {:useWebWorkers false})] + zreader) + + (instance? js/ArrayBuffer blob) + (reader (js/Uint8Array. blob)) :else - (->> (.async ^js entry type) - (p/fmap (fn [content] - ;; (js/console.log "zip:process-file" 2 content) - {:path path - :content content}))))) + (throw (ex-info "invalid arguments" + {:type :internal + :code :invalid-type})))) -(defn load - [data] - (rx/from (zip/loadAsync data))) +(defn blob-writer + [& {:keys [mtype]}] + (new zip/BlobWriter (or mtype "application/octet-stream"))) -(defn get-file - "Gets a single file from the zip archive" - ([zip path] - (get-file zip path "text")) +(defn bytes-writer + [] + (new zip/Uint8ArrayWriter)) - ([zip path type] - (-> (.file zip path) - (process-file path type) - (rx/from)))) +(defn writer + [stream-writer] + (new zip/ZipWriter stream-writer)) -(defn extract-files - "Creates a stream that will emit values for every file in the zip" - [zip] - (let [promises (atom []) - get-file - (fn [path entry] - (let [current (process-file entry path "text")] - (swap! promises conj current)))] - (.forEach zip get-file) +(defn add + [writer path content] + (assert (instance? zip/ZipWriter writer)) + (cond + (instance? js/Uint8Array content) + (.add writer path (new zip/Uint8ArrayReader content)) - (->> (rx/from (p/all @promises)) - (rx/merge-map identity)))) + (instance? js/ArrayBuffer content) + (.add writer path (new zip/Uint8ArrayReader + (new js/Uint8Array content))) + + (instance? js/Blob content) + (.add writer path (new zip/BlobReader content)) + + + (string? content) + (.add writer path (new zip/TextReader content)) + + :else + (throw (ex-info "invalid arguments" + {:type :internal + :code :invalid-type})))) + + +(defn get-entry + [reader path] + (assert (instance? zip/ZipReader reader)) + (->> (.getEntries ^zip/ZipReader reader) + (p/fmap (fn [entries] + (array/find #(= (.-filename ^js %) path) entries))))) + +(defn read-as-text + [entry] + (let [writer (new zip/TextWriter)] + (.getData entry writer))) + +(defn close + [closeable] + (assert (or (instance? zip/ZipReader closeable) + (instance? zip/ZipWriter closeable))) + (.close ^js closeable)) diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index 7c4a9969d..f120ea90f 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -6,444 +6,39 @@ (ns app.worker.export (:require - [app.common.data :as d] [app.common.exceptions :as ex] - [app.common.json :as json] - [app.common.media :as cm] - [app.common.text :as ct] - [app.common.types.components-list :as ctkl] - [app.common.types.file :as ctf] - [app.config :as cfg] - [app.main.features.pointer-map :as fpmap] - [app.main.render :as r] [app.main.repo :as rp] - [app.util.http :as http] [app.util.webapi :as wapi] - [app.util.zip :as uz] [app.worker.impl :as impl] - [beicon.v2.core :as rx] - [cuerdas.core :as str])) - -(def ^:const current-version 2) - -(defn create-manifest - "Creates a manifest entry for the given files" - [team-id file-id export-type files features] - (letfn [(format-page [manifest page] - (-> manifest - (assoc (str (:id page)) - {:name (:name page)}))) - - (format-file [manifest file] - (let [name (:name file) - is-shared (:is-shared file) - pages (->> (get-in file [:data :pages]) - (mapv str)) - index (->> (get-in file [:data :pages-index]) - (vals) - (reduce format-page {}))] - (-> manifest - (assoc (str (:id file)) - {:name name - :features features - :shared is-shared - :pages pages - :pagesIndex index - :version current-version - :libraries (->> (:libraries file) (into #{}) (mapv str)) - :exportType (d/name export-type) - :hasComponents (d/not-empty? (ctkl/components-seq (:data file))) - :hasDeletedComponents (d/not-empty? (ctkl/deleted-components-seq (:data file))) - :hasMedia (d/not-empty? (get-in file [:data :media])) - :hasColors (d/not-empty? (get-in file [:data :colors])) - :hasTypographies (d/not-empty? (get-in file [:data :typographies]))}))))] - (let [manifest {:teamId (str team-id) - :fileId (str file-id) - :files (->> (vals files) (reduce format-file {}))}] - (json/encode manifest)))) - -(defn process-pages [file] - (let [pages (get-in file [:data :pages]) - pages-index (get-in file [:data :pages-index])] - (->> pages - (map #(hash-map - :file-id (:id file) - :data (get pages-index %)))))) - -(defn get-page-data - [{file-id :file-id {:keys [id name] :as data} :data}] - (->> (r/render-page data) - (rx/map (fn [markup] - {:id id - :name name - :file-id file-id - :markup markup})))) - -(defn collect-page - [{:keys [id file-id markup] :as page}] - [(str file-id "/" id ".svg") markup]) - -(defn collect-entries [result data keys] - (-> result - (assoc (str (:id data)) - (->> (select-keys data keys) - (d/deep-mapm - (fn [[k v]] - [(-> k str/camel) v])))))) - -(def ^:const color-keys - [:name :color :opacity :gradient :path]) - -(def ^:const image-color-keys - [:width :height :mtype :name :keep-aspect-ratio]) - -(def ^:const typography-keys - [:name :font-family :font-id :font-size :font-style :font-variant-id :font-weight - :letter-spacing :line-height :text-transform :path]) - -(def ^:const media-keys - [:name :mtype :width :height :path]) - -(defn collect-color - [result color] - (let [id (str (:id color)) - basic-data (select-keys color color-keys) - image-color-data (when-let [image-color (:image color)] - (->> (select-keys image-color image-color-keys))) - color-data (cond-> basic-data - (some? image-color-data) - (-> - (assoc :image image-color-data) - (assoc-in [:image :id] (str (get-in color [:image :id])))))] - (-> result - (assoc id - (->> color-data - (d/deep-mapm - (fn [[k v]] - [(-> k str/camel) v]))))))) - -(defn collect-typography - [result typography] - (collect-entries result typography typography-keys)) - -(defn collect-media - [result media] - (collect-entries result media media-keys)) - -(defn parse-library-color - [[file-id colors]] - (rx/merge - (let [markup - (->> (vals colors) - (reduce collect-color {}) - (json/encode))] - (rx/of (vector (str file-id "/colors.json") markup))) - - (->> (rx/from (vals colors)) - (rx/map :image) - (rx/filter d/not-empty?) - (rx/merge-map - (fn [image-color] - (let [file-path (str/concat file-id "/colors/" (:id image-color) (cm/mtype->extension (:mtype image-color)))] - (->> (http/send! - {:uri (cfg/resolve-file-media image-color) - :response-type :blob - :method :get}) - (rx/map :body) - (rx/map #(vector file-path %))))))))) - -(defn parse-library-typographies - [[file-id typographies]] - (let [markup - (->> (vals typographies) - (reduce collect-typography {}) - (json/encode))] - [(str file-id "/typographies.json") markup])) - -(defn parse-library-media - [[file-id media]] - (rx/merge - (let [markup - (->> (vals media) - (reduce collect-media {}) - (json/encode))] - (rx/of (vector (str file-id "/media.json") markup))) - - (->> (rx/from (vals media)) - (rx/map #(assoc % :file-id file-id)) - (rx/merge-map - (fn [media] - (let [file-path (str/concat file-id "/media/" (:id media) (cm/mtype->extension (:mtype media)))] - (->> (http/send! - {:uri (cfg/resolve-file-media media) - :response-type :blob - :method :get}) - (rx/map :body) - (rx/map #(vector file-path %))))))))) - -(defn parse-library-components - [file] - (->> (r/render-components (:data file) false) - (rx/map #(vector (str (:id file) "/components.svg") %)))) - -(defn parse-deleted-components - [file] - (->> (r/render-components (:data file) true) - (rx/map #(vector (str (:id file) "/deleted-components.svg") %)))) - -(defn fetch-file-with-libraries - [file-id features] - (->> (rx/zip (->> (rp/cmd! :get-file {:id file-id :features features}) - (rx/mapcat fpmap/resolve-file)) - (rp/cmd! :get-file-libraries {:file-id file-id})) - (rx/map - (fn [[file file-libraries]] - (let [libraries-ids (->> file-libraries (map :id) (filterv #(not= (:id file) %)))] - (assoc file :libraries libraries-ids)))))) - -(defn make-local-external-references - [file file-id] - (let [change-fill - (fn [fill] - (cond-> fill - (not= file-id (:fill-color-ref-file fill)) - (assoc :fill-color-ref-file file-id))) - - change-stroke - (fn [stroke] - (cond-> stroke - (not= file-id (:stroke-color-ref-file stroke)) - (assoc :stroke-color-ref-file file-id))) - - change-text - (fn [content] - (->> content - (ct/transform-nodes - (fn [node] - (-> node - (d/update-when :fills #(mapv change-fill %)) - (cond-> (not= file-id (:typography-ref-file node)) - (assoc :typography-ref-file file-id))))))) - - change-shape - (fn [shape] - (-> shape - (d/update-when :fills #(mapv change-fill %)) - (d/update-when :strokes #(mapv change-stroke %)) - (cond-> (not= file-id (:component-file shape)) - (assoc :component-file file-id)) - - (cond-> (= :text (:type shape)) - (update :content change-text)))) - - change-objects - (fn [objects] - (->> objects - (d/mapm #(change-shape %2)))) - - change-pages - (fn [pages-index] - (->> pages-index - (d/mapm - (fn [_ data] - (-> data - (update :objects change-objects))))))] - (-> file - (update-in [:data :pages-index] change-pages)))) - -(defn merge-assets [target-file assets-files] - (let [merge-file-assets - (fn [target file] - (let [colors (get-in file [:data :colors]) - typographies (get-in file [:data :typographies]) - media (get-in file [:data :media]) - components (ctkl/components (:data file))] - (cond-> target - (d/not-empty? colors) - (update-in [:data :colors] merge colors) - - (d/not-empty? typographies) - (update-in [:data :typographies] merge typographies) - - (d/not-empty? media) - (update-in [:data :media] merge media) - - (d/not-empty? components) - (update-in [:data :components] merge components))))] - - (->> assets-files - (reduce merge-file-assets target-file)))) - -(defn process-export - [file-id export-type files] - - (let [result - (case export-type - :all files - :merge (let [file-list (-> files (d/without-keys [file-id]) vals)] - (-> (select-keys files [file-id]) - (update file-id merge-assets file-list) - (update file-id make-local-external-references file-id) - (update file-id dissoc :libraries))) - :detach (-> (select-keys files [file-id]) - (update file-id ctf/detach-external-references file-id) - (update file-id dissoc :libraries)))] - - ;;(.log js/console (clj->js result)) - result)) - -(defn collect-files - [file-id export-type features] - (letfn [(fetch-dependencies [[files pending]] - (if (empty? pending) - ;; When not pending, we finish the generation - (rx/empty) - - ;; Still pending files, fetch the next one - (let [next (peek pending) - pending (pop pending)] - (if (contains? files next) - ;; The file is already in the result - (rx/of [files pending]) - - (->> (fetch-file-with-libraries next features) - (rx/map - (fn [file] - [(-> files - (assoc (:id file) file)) - (as-> pending $ - (reduce conj $ (:libraries file)))])))))))] - (let [files {} - pending [file-id]] - (->> (rx/of [files pending]) - (rx/expand fetch-dependencies) - (rx/last) - (rx/map first) - (rx/map #(process-export file-id export-type %)))))) - -(defn export-file - [team-id file-id export-type features] - (let [files-stream (->> (collect-files file-id export-type features) - (rx/share)) - - manifest-stream - (->> files-stream - (rx/map #(create-manifest team-id file-id export-type % features)) - (rx/map #(vector "manifest.json" %))) - - render-stream - (->> files-stream - (rx/merge-map vals) - (rx/merge-map process-pages) - (rx/observe-on :async) - (rx/merge-map get-page-data) - (rx/share)) - - colors-stream - (->> files-stream - (rx/merge-map vals) - (rx/map #(vector (:id %) (get-in % [:data :colors]))) - (rx/filter #(d/not-empty? (second %))) - (rx/merge-map parse-library-color)) - - typographies-stream - (->> files-stream - (rx/merge-map vals) - (rx/map #(vector (:id %) (get-in % [:data :typographies]))) - (rx/filter #(d/not-empty? (second %))) - (rx/map parse-library-typographies)) - - media-stream - (->> files-stream - (rx/merge-map vals) - (rx/map #(vector (:id %) (get-in % [:data :media]))) - (rx/filter #(d/not-empty? (second %))) - (rx/merge-map parse-library-media)) - - components-stream - (->> files-stream - (rx/merge-map vals) - (rx/filter #(d/not-empty? (ctkl/components-seq (:data %)))) - (rx/merge-map parse-library-components)) - - deleted-components-stream - (->> files-stream - (rx/merge-map vals) - (rx/filter #(d/not-empty? (ctkl/deleted-components-seq (:data %)))) - (rx/merge-map parse-deleted-components)) - - pages-stream - (->> render-stream - (rx/map collect-page))] - - (rx/merge - (->> render-stream - (rx/map #(hash-map - :type :progress - :file file-id - :data (str "Render " (:file-name %) " - " (:name %))))) - - (->> (rx/merge - manifest-stream - pages-stream - components-stream - deleted-components-stream - media-stream - colors-stream - typographies-stream) - (rx/reduce conj []) - (rx/with-latest-from files-stream) - (rx/merge-map (fn [[data files]] - (->> (uz/compress-files data) - (rx/map #(vector (get files file-id) %))))))))) + [beicon.v2.core :as rx])) (defmethod impl/handler :export-files - [{:keys [team-id files type format features] :as message}] - (cond - (or (= format :binfile-v1) - (= format :binfile-v3)) - (->> (rx/from files) - (rx/mapcat - (fn [file] - (->> (rp/cmd! :export-binfile {:file-id (:id file) - :version (if (= format :binfile-v3) 3 1) - :include-libraries (= type :all) - :embed-assets (= type :merge)}) - (rx/map wapi/create-blob) - (rx/map wapi/create-uri) - (rx/map (fn [uri] - {:type :finish - :file-id (:id file) - :filename (:name file) - :mtype (if (= format :binfile-v3) - "application/zip" - "application/penpot") - :uri uri})) - (rx/catch - (fn [cause] - (rx/of {:type :error - :file-id (:id file) - :hint (ex-message cause)}))))))) + [{:keys [files type format] :as message}] + (assert (or (= format :binfile-v1) + (= format :binfile-v3)) + "expected valid format") - (= format :legacy-zip) - (->> (rx/from files) - (rx/mapcat - (fn [file] - (->> (export-file team-id (:id file) type features) - (rx/map - (fn [value] - (if (contains? value :type) - value - (let [[file export-blob] value] - {:type :finish - :file-id (:id file) - :filename (:name file) - :mtype "application/zip" - :uri (wapi/create-uri export-blob)})))) - (rx/catch - (fn [cause] - (rx/of (ex/raise :type :internal - :code :export-error - :hint "unexpected error on exporting file" - :file-id (:id file) - :cause cause)))))))))) + (->> (rx/from files) + (rx/mapcat + (fn [file] + (->> (rp/cmd! :export-binfile {:file-id (:id file) + :version (if (= format :binfile-v3) 3 1) + :include-libraries (= type :all) + :embed-assets (= type :merge)}) + (rx/map wapi/create-blob) + (rx/map wapi/create-uri) + (rx/map (fn [uri] + {:type :finish + :file-id (:id file) + :filename (:name file) + :mtype (if (= format :binfile-v3) + "application/zip" + "application/penpot") + :uri uri})) + (rx/catch + (fn [cause] + (rx/of (ex/raise :type :internal + :code :export-error + :hint "unexpected error on exporting file" + :file-id (:id file) + :cause cause))))))))) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 0432b23d9..49a1fcd81 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -30,9 +30,9 @@ (def conjv (fnil conj [])) (defn- read-zip-manifest - [zipfile] - (->> (uz/get-file zipfile "manifest.json") - (rx/map :content) + [zip-reader] + (->> (rx/from (uz/get-entry zip-reader "manifest.json")) + (rx/mapcat uz/read-as-text) (rx/map json/decode))) (defn slurp-uri @@ -121,14 +121,15 @@ (let [mtype (parse-mtype body)] (cond (= "application/zip" mtype) - (->> (uz/load body) - (rx/merge-map read-zip-manifest) - (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 :legacy-zip :body body))))) + (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 :legacy-zip :body body)))) + (rx/finalize (partial uz/close zip-reader)))) (= "application/octet-stream" mtype) (rx/of (assoc file :type :binfile-v1)) diff --git a/frontend/src/lib/file_builder.cljs b/frontend/src/lib/file_builder.cljs deleted file mode 100644 index cf09d7ee2..000000000 --- a/frontend/src/lib/file_builder.cljs +++ /dev/null @@ -1,250 +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 INC - -(ns lib.file-builder - (: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.schema :as sm] - [app.common.uuid :as uuid] - [app.util.object :as obj])) - -(def BuilderError - (obj/class - :name "BuilderError" - :extends js/Error - :constructor - (fn [this type code hint cause] - (.call js/Error this hint) - (set! (.-name this) (str "Exception: " hint)) - (set! (.-type this) type) - (set! (.-code this) code) - (set! (.-hint this) hint) - - (when (exists? js/Error.captureStackTrace) - (.captureStackTrace js/Error this)) - - (obj/add-properties! - this - {:name "cause" - :enumerable true - :this false - :get (fn [] cause)} - {:name "data" - :enumerable true - :this false - :get (fn [] - (let [data (ex-data cause)] - (when-let [explain (::sm/explain data)] - (json/->js (sm/simplify explain)))))})))) - -(defn- handle-exception - [cause] - (let [data (ex-data cause)] - (throw (new BuilderError - (d/name (get data :type :unknown)) - (d/name (get data :code :unknown)) - (or (get data :hint) (ex-message cause)) - cause)))) - -(defn- decode-params - [params] - (if (obj/plain-object? params) - (json/->js params) - params)) - -(defn- create-file* - [file] - (let [state* (volatile! file)] - (obj/reify {:name "File"} - :id - {:get #(dm/str (:id @state*))} - - :currentFrameId - {:get #(dm/str (::fb/current-frame-id @state*))} - - :currentPageId - {:get #(dm/str (::fb/current-page-id @state*))} - - :lastId - {:get #(dm/str (::fb/last-id @state*))} - - :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)))) - - :closePage - (fn [] - (vswap! state* fb/close-page)) - - :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)))) - - :closeArtboard - (fn [] - (vswap! state* fb/close-artboard)) - - :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)))) - - :closeGroup - (fn [] - (vswap! state* fb/close-group)) - - :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)))) - - :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)))) - - :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)))) - - :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)))) - - :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)))) - - :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)))) - - :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)))) - - :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)))) - - :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)))) - - :getShape - (fn [shape-id] - (let [shape-id (uuid/parse shape-id)] - (some-> (fb/lookup-shape @state* shape-id) - (json/->js)))) - - :toMap - (fn [] - (-> @state* - (d/without-qualified) - (json/->js)))))) - -(defn create-file - [params] - (try - (let [params (-> params json/->clj fb/decode-file) - file (fb/create-file params)] - (create-file* file)) - (catch :default cause - (handle-exception cause)))) diff --git a/frontend/src/lib/playground/sample1.js b/frontend/src/lib/playground/sample1.js deleted file mode 100644 index 8dd7c8799..000000000 --- a/frontend/src/lib/playground/sample1.js +++ /dev/null @@ -1,30 +0,0 @@ -import * as penpot from "../../../target/library/penpot.js"; - -console.log(penpot); - -try { - 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}); - - // console.dir(file.toMap(), {depth:10}); -} catch (e) { - console.log(e); - // console.log(e.data); -} - -process.exit(0); diff --git a/frontend/test/frontend_tests/util_snap_data_test.cljs b/frontend/test/frontend_tests/util_snap_data_test.cljs index f46fba7d6..0ad546a67 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/frontend/yarn.lock b/frontend/yarn.lock index e347c2465..bb6715974 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2881,10 +2881,17 @@ __metadata: languageName: node linkType: hard -"@zip.js/zip.js@npm:^2.7.44": - version: 2.7.53 - resolution: "@zip.js/zip.js@npm:2.7.53" - checksum: 10c0/883527bf09ce7c312117536c79d5f07e736d87de802a6c19e39ba2e18027499dcb9359df94dfde13c9bcf6118a20b4f26a40f9892ee82d7cac3124d6986b15c8 +"@zip.js/zip.js@npm:2.7.60": + version: 2.7.60 + resolution: "@zip.js/zip.js@npm:2.7.60" + checksum: 10c0/466ff1729e36d9f500011475e230f2edb9c0e6e10f64d542e6ebc006dc70885bc909d69fd0c7b10126bdf722c761359ad1edfe295b6c7fed3169f0f63012a1cd + languageName: node + linkType: hard + +"@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": + version: 2.7.60 + resolution: "@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::version=2.7.60&hash=4a67b2" + checksum: 10c0/37e9a5dd708fd81b08d64b75ea44d70c903071165bbbc571fca7a1cb93f214fab6f63ed3c837a87a0205a6301bc78790c1505197570f112afa5311e3a16d2368 languageName: node linkType: hard @@ -5705,6 +5712,7 @@ __metadata: "@storybook/test-runner": "npm:^0.21.0" "@tokens-studio/sd-transforms": "npm:1.2.11" "@types/node": "npm:^22.12.0" + "@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" autoprefixer: "npm:^10.4.20" compression: "npm:^1.7.5" concurrently: "npm:^9.1.2" @@ -5724,7 +5732,6 @@ __metadata: gulp-svg-sprite: "npm:^2.0.3" js-beautify: "npm:^1.15.4" jsdom: "npm:^26.1.0" - jszip: "npm:^3.10.1" lodash: "npm:^4.17.21" lodash.debounce: "npm:^4.0.8" luxon: "npm:^3.6.1" @@ -6386,13 +6393,6 @@ __metadata: languageName: node linkType: hard -"immediate@npm:~3.0.5": - version: 3.0.6 - resolution: "immediate@npm:3.0.6" - checksum: 10c0/f8ba7ede69bee9260241ad078d2d535848745ff5f6995c7c7cb41cfdc9ccc213f66e10fa5afb881f90298b24a3f7344b637b592beb4f54e582770cdce3f1f039 - languageName: node - linkType: hard - "immutable@npm:^5.0.2": version: 5.0.3 resolution: "immutable@npm:5.0.3" @@ -7686,18 +7686,6 @@ __metadata: languageName: node linkType: hard -"jszip@npm:^3.10.1": - version: 3.10.1 - resolution: "jszip@npm:3.10.1" - dependencies: - lie: "npm:~3.3.0" - pako: "npm:~1.0.2" - readable-stream: "npm:~2.3.6" - setimmediate: "npm:^1.0.5" - checksum: 10c0/58e01ec9c4960383fb8b38dd5f67b83ccc1ec215bf74c8a5b32f42b6e5fb79fada5176842a11409c4051b5b94275044851814a31076bf49e1be218d3ef57c863 - languageName: node - linkType: hard - "klaw-sync@npm:^6.0.0": version: 6.0.0 resolution: "klaw-sync@npm:6.0.0" @@ -7728,15 +7716,6 @@ __metadata: languageName: node linkType: hard -"lie@npm:~3.3.0": - version: 3.3.0 - resolution: "lie@npm:3.3.0" - dependencies: - immediate: "npm:~3.0.5" - checksum: 10c0/56dd113091978f82f9dc5081769c6f3b947852ecf9feccaf83e14a123bc630c2301439ce6182521e5fbafbde88e88ac38314327a4e0493a1bea7e0699a7af808 - languageName: node - linkType: hard - "lilconfig@npm:^3.1.1": version: 3.1.2 resolution: "lilconfig@npm:3.1.2" @@ -8812,13 +8791,6 @@ __metadata: languageName: node linkType: hard -"pako@npm:~1.0.2": - version: 1.0.11 - resolution: "pako@npm:1.0.11" - checksum: 10c0/86dd99d8b34c3930345b8bbeb5e1cd8a05f608eeb40967b293f72fe469d0e9c88b783a8777e4cc7dc7c91ce54c5e93d88ff4b4f060e6ff18408fd21030d9ffbe - languageName: node - linkType: hard - "parse-json@npm:^4.0.0": version: 4.0.0 resolution: "parse-json@npm:4.0.0" diff --git a/library/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch b/library/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch new file mode 100644 index 000000000..2a413b389 --- /dev/null +++ b/library/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch @@ -0,0 +1,18 @@ +diff --git a/lib/zip-fs.js b/lib/zip-fs.js +index 1444c0f00e5f1ad6c13521f90a7f3c6659d81116..90e38baef5365c2abbcb9337f7ab37f800e883a4 100644 +--- a/lib/zip-fs.js ++++ b/lib/zip-fs.js +@@ -33,12 +33,7 @@ import { initShimAsyncCodec } from "./core/util/stream-codec-shim.js"; + import { terminateWorkers } from "./core/codec-pool.js"; + + let baseURL; +-try { +- baseURL = import.meta.url; +- // eslint-disable-next-line no-unused-vars +-} catch (_) { +- // ignored +-} ++ + configure({ baseURL }); + configureWebWorker(configure); + diff --git a/library/deps.edn b/library/deps.edn new file mode 100644 index 000000000..af9544ec6 --- /dev/null +++ b/library/deps.edn @@ -0,0 +1,33 @@ +{:paths ["src" "vendor" "resources" "test"] + :deps + {penpot/common + {:local/root "../common"} + + penpot/frontend + {:local/root "../frontend"} + } + + :aliases + {:outdated + {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}} + :main-opts ["-m" "antq.core"]} + + :jvm-repl + {:extra-deps + {com.bhauman/rebel-readline {:mvn/version "RELEASE"}} + :main-opts ["-m" "rebel-readline.main"] + :jvm-opts ["--sun-misc-unsafe-memory-access=allow"]} + + :dev + {:extra-paths ["dev"] + :extra-deps + {thheller/shadow-cljs {:mvn/version "3.0.5"} + com.bhauman/rebel-readline {:mvn/version "RELEASE"} + org.clojure/tools.namespace {:mvn/version "RELEASE"} + criterium/criterium {:mvn/version "RELEASE"} + cider/cider-nrepl {:mvn/version "0.48.0"}}} + + :shadow-cljs + {:main-opts ["-m" "shadow.cljs.devtools.cli"] + :jvm-opts ["--sun-misc-unsafe-memory-access=allow"]} + }} diff --git a/library/package.json b/library/package.json new file mode 100644 index 000000000..0ee94c5a8 --- /dev/null +++ b/library/package.json @@ -0,0 +1,42 @@ +{ + "name": "@penpotapp/library", + "version": "1.0.0", + "license": "MPL-2.0", + "author": "Kaleidos INC", + "private": true, + "packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538", + "type": "module", + "repository": { + "type": "git", + "url": "https://github.com/penpot/penpot" + }, + "resolutions": { + "@zip.js/zip.js@npm:^2.7.44": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch" + }, + "imports": { + "#self": { + "default": "./target/library/penpot.js" + } + }, + "scripts": { + "clear:shadow-cache": "rm -rf .shadow-cljs", + "build": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs release library", + "fmt:clj": "cljfmt fix --parallel=true src/ test/", + "fmt:clj:check": "cljfmt check --parallel=false src/ test/", + "lint:clj": "clj-kondo --parallel --lint src/", + "test": "node --test", + "watch:test": "node --test --watch", + "watch": "yarn run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library" + }, + "devDependencies": { + "@types/node": "^22.12.0", + "concurrently": "^9.1.2", + "nodemon": "^3.1.9", + "shadow-cljs": "3.0.5" + }, + "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", + "source-map-support": "^0.5.21" + } +} diff --git a/library/playground/components.js b/library/playground/components.js new file mode 100644 index 000000000..8b400b761 --- /dev/null +++ b/library/playground/components.js @@ -0,0 +1,103 @@ +import * as penpot from "#self"; +import { createWriteStream } from 'fs'; +import { Writable } from "stream"; + +// Example of creating component and instance out of order + +(async function() { + const context = penpot.createBuildContext(); + + { + context.addFile({name: "Test File 1"}); + context.addPage({name: "Foo Page"}) + + const mainBoardId = context.genId(); + const mainRectId = context.genId(); + + // First create instance (just for with the purpose of teaching + // that it can be done, without putting that under obligation to + // do it in this order or the opposite) + + context.addBoard({ + name: "Board Instance 1", + x: 700, + y: 0, + width: 500, + height: 300, + shapeRef: mainBoardId, + touched: ["name-group"] + }) + + context.addRect({ + name: "Rect Instance 1", + x: 800, + y: 20, + width:100, + height:200, + shapeRef: mainRectId, + touched: ["name-group"] + }); + + // this function call takes the current board from context, but it + // also can be passed as parameter on an explicit way if you + // prefer + context.addComponentInstance({ + componentId: "00000000-0000-0000-0000-000000000001" + }); + + context.closeBoard(); + + // Then, create the main instance + context.addBoard({ + id: mainBoardId, + name: "Board", + x: 0, + y: 0, + width: 500, + height: 300, + }) + + context.addRect({ + id: mainRectId, + name: "Rect 1", + x: 20, + y: 20, + width:100, + height:200, + }); + + context.addComponent({ + componentId: "00000000-0000-0000-0000-000000000001", + name: "Component 1", + }); + + context.closeBoard(); + context.closeFile(); + } + + { + // Create a file stream to write the zip to + const output = createWriteStream('sample-with-components.zip'); + // Wrap Node's stream in a WHATWG WritableStream + const writable = Writable.toWeb(output); + await penpot.exportStream(context, writable); + } + +})().catch((cause) => { + console.error(cause); + + const causeExplain = cause.explain; + if (causeExplain) { + console.log("EXPLAIN:") + console.error(cause.explain); + } + + // const innerCause = cause.cause; + // if (innerCause) { + // console.log("INNER:"); + // console.error(innerCause); + // } + process.exit(-1); +}).finally(() => { + process.exit(0); +}) diff --git a/library/playground/sample.jpg b/library/playground/sample.jpg new file mode 100644 index 000000000..947698852 Binary files /dev/null and b/library/playground/sample.jpg differ diff --git a/library/playground/sample1.js b/library/playground/sample1.js new file mode 100644 index 000000000..0f90bb32e --- /dev/null +++ b/library/playground/sample1.js @@ -0,0 +1,88 @@ +import * as penpot from "#self"; +import { writeFile, readFile } from 'fs/promises'; +import { createWriteStream } from 'fs'; +import { Writable } from "stream"; + +// console.log(penpot); + +(async function() { + const context = penpot.createBuildContext(); + + { + 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(context, writable); + // } + +})().catch((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/scripts/repl b/library/scripts/repl new file mode 100755 index 000000000..bf9f4065f --- /dev/null +++ b/library/scripts/repl @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +export OPTIONS="-A:dev -J-XX:-OmitStackTraceInFastThrow"; + +set -ex +exec clojure $OPTIONS -M -m rebel-readline.main diff --git a/library/shadow-cljs.edn b/library/shadow-cljs.edn new file mode 100644 index 000000000..5eee3a699 --- /dev/null +++ b/library/shadow-cljs.edn @@ -0,0 +1,55 @@ +{:deps {:aliases [:dev]} + :http {:port #shadow/env ["HTTP_PORT" :as :int :default 4448]} + :dev-http {#shadow/env ["DEV_PORT" :as :int :default 8889] "classpath:public"} + :nrepl false + :socket-repl false + :cache-dir #shadow/env ["CACHE" :default ".shadow-cljs"] + + :builds + {:test + {:target :esm + :output-dir "target/tests" + :runtime :custom + :js-options {:js-provider :import} + + :modules + {:test {:init-fn lib.tests.runner/init + :prepend-js ";if (typeof globalThis.navigator?.userAgent === 'undefined') { globalThis.navigator = {userAgent: ''}; };"}}} + + :library + {:target :esm + :runtime :custom + :output-dir "target/library" + :devtools {:autoload false} + + :modules + {:penpot + {:exports {BuilderError lib.builder/BuilderError + createBuildContext lib.builder/create-build-context + exportAsBytes lib.export/export-bytes + exportAsBlob lib.export/export-blob + exportStream lib.export/export-stream + }}} + + :js-options + {:entry-keys ["module" "browser" "main"] + :export-conditions ["module" "import", "browser" "require" "default"] + ;; :js-provider :import + ;; :external-index "target/library/dependencies.js" + ;; :external-index-format :esm + } + + :compiler-options + {:output-feature-set :es2020 + :output-wrapper false + :warnings {:fn-deprecated false}} + + :release + {:compiler-options + {:fn-invoke-direct true + :optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced] + :pretty-print false + :source-map true + :elide-asserts true + :anon-fn-naming-policy :off + :source-map-detail-level :all}}}}} diff --git a/library/src/lib/builder.cljs b/library/src/lib/builder.cljs new file mode 100644 index 000000000..df9b909cb --- /dev/null +++ b/library/src/lib/builder.cljs @@ -0,0 +1,292 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns lib.builder + (: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.schema :as sm] + [app.common.uuid :as uuid] + [app.util.object :as obj])) + +(def BuilderError + (obj/class + :name "BuilderError" + :extends js/Error + :constructor + (fn [this type code hint cause] + (.call js/Error this hint) + (set! (.-name this) (str "Exception: " hint)) + (set! (.-type this) type) + (set! (.-code this) code) + (set! (.-hint this) hint) + + (when (exists? js/Error.captureStackTrace) + (.captureStackTrace js/Error this)) + + (obj/add-properties! + this + {:name "cause" + :enumerable true + :this false + :get (fn [] cause)} + {:name "explain" + :enumerable true + :this false + :get (fn [] + (let [data (ex-data cause)] + (when-let [explain (::sm/explain data)] + (json/->js (sm/simplify explain)))))})))) + +(defn- handle-exception + [cause] + (let [data (ex-data cause)] + (throw (new BuilderError + (d/name (get data :type :unknown)) + (d/name (get data :code :unknown)) + (or (get data :hint) (ex-message cause)) + cause)))) + +(defn- decode-params + [params] + (if (obj/plain-object? params) + (json/->clj params) + params)) + +(defn- get-current-page-id + [state] + (dm/str (get state ::fb/current-page-id))) + +(defn- get-last-id + [state] + (dm/str (get state ::fb/last-id))) + +(defn- create-builder-api + [state] + (obj/reify {:name "BuildContext"} + :currentFileId + {:get #(dm/str (get @state ::fb/current-file-id))} + + :currentFrameId + {:get #(dm/str (get @state ::fb/current-frame-id))} + + :currentPageId + {:get #(get-current-page-id @state)} + + :lastId + {:get #(get-last-id @state)} + + :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)))) + + :closeFile + (fn [] + (swap! state fb/close-file) + nil) + + :addPage + (fn [params] + (try + (let [params (-> (decode-params params) + (fb/decode-page))] + + (-> (swap! state fb/add-page params) + (get-current-page-id))) + + (catch :default cause + (handle-exception cause)))) + + :closePage + (fn [] + (swap! state fb/close-page) + nil) + + :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)))) + + :closeBoard + (fn [] + (swap! state fb/close-board) + nil) + + :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)))) + + :closeGroup + (fn [] + (swap! state fb/close-group) + nil) + + :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)))) + + :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)))) + + :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)))) + + :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)))) + + :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))) + + :getInternalState + (fn [] + (json/->js @state)))) + +(defn create-build-context + "Create an empty builder state context." + [] + (let [state (atom {}) + api (create-builder-api state)] + + (specify! api + cljs.core/IDeref + (-deref [_] @state)))) diff --git a/library/src/lib/export.cljs b/library/src/lib/export.cljs new file mode 100644 index 000000000..f2b7fec26 --- /dev/null +++ b/library/src/lib/export.cljs @@ -0,0 +1,214 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns lib.export + "A .penpot export implementation" + (:require + [app.common.data :as d] + [app.common.files.builder :as fb] + [app.common.json :as json] + [app.common.media :as media] + [app.common.schema :as sm] + [app.common.types.color :as types.color] + [app.common.types.component :as types.component] + [app.common.types.file :as types.file] + [app.common.types.page :as types.page] + [app.common.types.plugins :as ctpg] + [app.common.types.shape :as types.shape] + [app.common.types.tokens-lib :as types.tokens-lib] + [app.common.types.typography :as types.typography] + [app.util.zip :as zip] + [cuerdas.core :as str] + [promesa.core :as p])) + +(def ^:private schema:file + [:merge + types.file/schema:file + [:map [:options {:optional true} types.file/schema:options]]]) + +(def ^:private encode-file + (sm/encoder schema:file sm/json-transformer)) + +(def ^:private encode-page + (sm/encoder types.page/schema:page sm/json-transformer)) + +(def ^:private encode-shape + (sm/encoder types.shape/schema:shape sm/json-transformer)) + +(def ^:private encode-component + (sm/encoder types.component/schema:component sm/json-transformer)) + +(def encode-color + (sm/encoder types.color/schema:color sm/json-transformer)) + +(def encode-typography + (sm/encoder types.typography/schema:typography sm/json-transformer)) + +(def encode-tokens-lib + (sm/encoder types.tokens-lib/schema:tokens-lib sm/json-transformer)) + +(def encode-plugin-data + (sm/encoder ::ctpg/plugin-data sm/json-transformer)) + +(def ^:private valid-buckets + #{"file-media-object" + "team-font-variant" + "file-object-thumbnail" + "file-thumbnail" + "profile" + "file-data" + "file-data-fragment" + "file-change"}) + +(def ^:private schema:storage-object + [:map {:title "StorageObject"} + [:id ::sm/uuid] + [:size ::sm/int] + [:content-type :string] + [:bucket [::sm/one-of {:format :string} valid-buckets]] + [:hash :string]]) + +(def encode-storage-object + (sm/encoder schema:storage-object sm/json-transformer)) + +(def ^:private file-attrs + #{:id + :name + :migrations + :features + :is-shared + :version}) + +(defn- generate-file-export-procs + [{:keys [id data] :as file}] + (cons + (let [file (cond-> (select-keys file file-attrs) + (:options data) + (assoc :options (:options data)))] + [(str "files/" id ".json") + (delay (-> file encode-file json/encode))]) + + (concat + (let [pages (get data :pages) + pages-index (get data :pages-index)] + + (->> (d/enumerate pages) + (mapcat + (fn [[index page-id]] + (let [page (get pages-index page-id) + objects (:objects page) + page (-> page + (dissoc :objects) + (assoc :index index))] + (cons + [(str "files/" id "/pages/" page-id ".json") + (delay (-> page encode-page json/encode))] + (map (fn [[shape-id shape]] + (let [shape (assoc shape :page-id page-id)] + [(str "files/" id "/pages/" page-id "/" shape-id ".json") + (delay (-> shape encode-shape json/encode))])) + objects))))))) + + (->> (get data :components) + (map (fn [[component-id component]] + [(str "files/" id "/components/" component-id ".json") + (delay (-> component encode-component json/encode))]))) + + (->> (get data :colors) + (map (fn [[color-id color]] + [(str "files/" id "/colors/" color-id ".json") + (delay (let [color (-> color + encode-color + (dissoc :file-id))] + (cond-> color + (and (contains? color :path) + (str/empty? (:path color))) + (dissoc :path) + + :always + (json/encode))))]))) + + (->> (get data :typographies) + (map (fn [[typography-id typography]] + [(str "files/" id "/typographies/" typography-id ".json") + (delay (-> typography + encode-typography + json/encode))]))) + + (when-let [tokens-lib (get data :tokens-lib)] + (list [(str "files/" id "/tokens.json") + (delay (-> tokens-lib + encode-tokens-lib + json/encode))]))))) + +(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 files + :relations []}] + ["manifest.json" (delay (json/encode params))])) + +(defn- export + [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 + (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 + [state] + (export state (zip/writer (zip/bytes-writer)))) + +(defn export-blob + [state] + (export state (zip/writer (zip/blob-writer)))) + +(defn export-stream + [state stream] + (export state (zip/writer stream))) diff --git a/library/test/builder.test.js b/library/test/builder.test.js new file mode 100644 index 000000000..de94f5c3a --- /dev/null +++ b/library/test/builder.test.js @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import * as penpot from "#self"; + +test("create empty context", () => { + const context = penpot.createBuildContext(); + assert.ok(context); +}); + +test("create context with single file", () => { + const context = penpot.createBuildContext(); + context.addFile({name: "sample"}); + + const internalState = context.getInternalState(); + + // console.log(internalState); + + assert.ok(internalState.files); + assert.equal(typeof internalState.files, "object"); + assert.equal(typeof internalState.currentFileId, "string"); + + const file = internalState.files[internalState.currentFileId]; + assert.ok(file); +}); + +test("create context with two file", () => { + const context = penpot.createBuildContext(); + + const fileId_1 = context.addFile({name: "sample 1"}); + const fileId_2 = context.addFile({name: "sample 2"}); + + const internalState = context.getInternalState(); + + // console.log(internalState.files[fileId_1]) + + assert.ok(internalState.files[fileId_1]); + assert.ok(internalState.files[fileId_2]); + assert.equal(internalState.files[fileId_1].name, "sample 1"); + assert.equal(internalState.files[fileId_2].name, "sample 2"); + + const file = internalState.files[fileId_2]; + + assert.ok(file.data); + assert.ok(file.data.pages); + assert.ok(file.data.pagesIndex); + assert.equal(file.data.pages.length, 0) +}); + +test("create context with file and page", () => { + const context = penpot.createBuildContext(); + + const fileId = context.addFile({name: "file 1"}); + const pageId = context.addPage({name: "page 1"}); + + const internalState = context.getInternalState(); + + const file = internalState.files[fileId]; + + assert.ok(file, "file should exist"); + + assert.ok(file.data); + assert.ok(file.data.pages); + + assert.equal(file.data.pages.length, 1); + + const page = file.data.pagesIndex[pageId]; + + assert.ok(page, "page should exist"); + assert.ok(page.objects, "page objects should exist"); + assert.equal(page.id, pageId); + + + const rootShape = page.objects["00000000-0000-0000-0000-000000000000"]; + assert.ok(rootShape, "root shape should exist"); + assert.equal(rootShape.id, "00000000-0000-0000-0000-000000000000"); +}); diff --git a/library/yarn.lock b/library/yarn.lock new file mode 100644 index 000000000..44b445a74 --- /dev/null +++ b/library/yarn.lock @@ -0,0 +1,1352 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: "npm:^7.0.4" + checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 + languageName: node + linkType: hard + +"@npmcli/agent@npm:^3.0.0": + version: 3.0.0 + resolution: "@npmcli/agent@npm:3.0.0" + dependencies: + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^10.0.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/efe37b982f30740ee77696a80c196912c274ecd2cb243bc6ae7053a50c733ce0f6c09fda085145f33ecf453be19654acca74b69e81eaad4c90f00ccffe2f9271 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/fs@npm:4.0.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/c90935d5ce670c87b6b14fab04a965a3b8137e585f8b2a6257263bd7f97756dd736cb165bb470e5156a9e718ecd99413dccc54b1138c1a46d6ec7cf325982fe5 + languageName: node + linkType: hard + +"@penpotapp/library@workspace:.": + version: 0.0.0-use.local + resolution: "@penpotapp/library@workspace:." + dependencies: + "@types/node": "npm:^22.12.0" + "@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" + concurrently: "npm:^9.1.2" + luxon: "npm:^3.6.1" + nodemon: "npm:^3.1.9" + shadow-cljs: "npm:3.0.5" + source-map-support: "npm:^0.5.21" + languageName: unknown + linkType: soft + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + +"@types/node@npm:^22.12.0": + version: 22.15.18 + resolution: "@types/node@npm:22.15.18" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10c0/e23178c568e2dc6b93b6aa3b8dfb45f9556e527918c947fe7406a4c92d2184c7396558912400c3b1b8d0fa952ec63819aca2b8e4d3545455fc6f1e9623e09ca6 + languageName: node + linkType: hard + +"@zip.js/zip.js@npm:2.7.60": + version: 2.7.60 + resolution: "@zip.js/zip.js@npm:2.7.60" + checksum: 10c0/466ff1729e36d9f500011475e230f2edb9c0e6e10f64d542e6ebc006dc70885bc909d69fd0c7b10126bdf722c761359ad1edfe295b6c7fed3169f0f63012a1cd + languageName: node + linkType: hard + +"@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": + version: 2.7.60 + resolution: "@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::version=2.7.60&hash=4a67b2" + checksum: 10c0/37e9a5dd708fd81b08d64b75ea44d70c903071165bbbc571fca7a1cb93f214fab6f63ed3c837a87a0205a6301bc78790c1505197570f112afa5311e3a16d2368 + languageName: node + linkType: hard + +"abbrev@npm:^3.0.0": + version: 3.0.1 + resolution: "abbrev@npm:3.0.1" + checksum: 10c0/21ba8f574ea57a3106d6d35623f2c4a9111d9ee3e9a5be47baed46ec2457d2eac46e07a5c4a60186f88cb98abbe3e24f2d4cca70bc2b12f1692523e2209a9ccf + languageName: node + linkType: hard + +"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": + version: 7.1.3 + resolution: "agent-base@npm:7.1.3" + checksum: 10c0/6192b580c5b1d8fb399b9c62bf8343d76654c2dd62afcb9a52b2cf44a8b6ace1e3b704d3fe3547d91555c857d3df02603341ff2cb961b9cfe2b12f9f3c38ee11 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.1.0 + resolution: "ansi-regex@npm:6.1.0" + checksum: 10c0/a91daeddd54746338478eef88af3439a7edf30f8e23196e2d6ed182da9add559c601266dbef01c2efa46a958ad6f1f8b176799657616c702b5b02e799e7fd8dc + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + languageName: node + linkType: hard + +"anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + +"binary-extensions@npm:^2.0.0": + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.11 + resolution: "brace-expansion@npm:1.1.11" + dependencies: + balanced-match: "npm:^1.0.0" + concat-map: "npm:0.0.1" + checksum: 10c0/695a56cd058096a7cb71fb09d9d6a7070113c7be516699ed361317aca2ec169f618e28b8af352e02ab4233fb54eb0168460a40dc320bab0034b36ab59aaad668 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + languageName: node + linkType: hard + +"braces@npm:~3.0.2": + version: 3.0.3 + resolution: "braces@npm:3.0.3" + dependencies: + fill-range: "npm:^7.1.1" + checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 + languageName: node + linkType: hard + +"buffer-from@npm:^1.0.0": + version: 1.1.2 + resolution: "buffer-from@npm:1.1.2" + checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 + languageName: node + linkType: hard + +"cacache@npm:^19.0.1": + version: 19.0.1 + resolution: "cacache@npm:19.0.1" + dependencies: + "@npmcli/fs": "npm:^4.0.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^10.0.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^2.0.1" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^7.0.2" + ssri: "npm:^12.0.0" + tar: "npm:^7.4.3" + unique-filename: "npm:^4.0.0" + checksum: 10c0/01f2134e1bd7d3ab68be851df96c8d63b492b1853b67f2eecb2c37bb682d37cb70bb858a16f2f0554d3c0071be6dfe21456a1ff6fa4b7eed996570d6a25ffe9c + languageName: node + linkType: hard + +"chalk@npm:^4.1.2": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880 + languageName: node + linkType: hard + +"chokidar@npm:^3.5.2": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 + languageName: node + linkType: hard + +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 + languageName: node + linkType: hard + +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^7.0.0" + checksum: 10c0/4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f + languageName: node + linkType: hard + +"concurrently@npm:^9.1.2": + version: 9.1.2 + resolution: "concurrently@npm:9.1.2" + dependencies: + chalk: "npm:^4.1.2" + lodash: "npm:^4.17.21" + rxjs: "npm:^7.8.1" + shell-quote: "npm:^1.8.1" + supports-color: "npm:^8.1.1" + tree-kill: "npm:^1.2.2" + yargs: "npm:^17.7.2" + bin: + conc: dist/bin/concurrently.js + concurrently: dist/bin/concurrently.js + checksum: 10c0/88e00269366aa885ca2b97fd53b04e7af2b0f31774d991bfc0e88c0de61cdebdf115ddacc9c897fbd1f1b90369014637fa77045a171d072a75693332b36dcc70 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.6": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 10c0/053ea8b2135caff68a9e81470e845613e374e7309a47731e81639de3eaeb90c3d01af0e0b44d2ab9d50b43467223b88567dfeb3262db942dc063b9976718ffc1 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4, debug@npm:^4.3.4": + version: 4.4.1 + resolution: "debug@npm:4.4.1" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"escalade@npm:^3.1.1": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.2 + resolution: "exponential-backoff@npm:3.1.2" + checksum: 10c0/d9d3e1eafa21b78464297df91f1776f7fbaa3d5e3f7f0995648ca5b89c069d17055033817348d9f4a43d1c20b0eab84f75af6991751e839df53e4dfd6f22e844 + languageName: node + linkType: hard + +"fdir@npm:^6.4.4": + version: 6.4.4 + resolution: "fdir@npm:6.4.4" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/6ccc33be16945ee7bc841e1b4178c0b4cf18d3804894cb482aa514651c962a162f96da7ffc6ebfaf0df311689fb70091b04dd6caffe28d56b9ebdc0e7ccadfdd + languageName: node + linkType: hard + +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.3.1 + resolution: "foreground-child@npm:3.3.1" + dependencies: + cross-spawn: "npm:^7.0.6" + signal-exit: "npm:^4.0.1" + checksum: 10c0/8986e4af2430896e65bc2788d6679067294d6aee9545daefc84923a0a4b399ad9c7a3ea7bd8c0b2b80fdf4a92de4c69df3f628233ff3224260e9c1541a9e9ed3 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + languageName: node + linkType: hard + +"fsevents@npm:~2.3.2": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + conditions: os=darwin + languageName: node + linkType: hard + +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + +"glob-parent@npm:~5.1.2": + version: 5.1.2 + resolution: "glob-parent@npm:5.1.2" + dependencies: + is-glob: "npm:^4.0.1" + checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee + languageName: node + linkType: hard + +"glob@npm:^10.2.2": + version: 10.4.5 + resolution: "glob@npm:10.4.5" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + languageName: node + linkType: hard + +"has-flag@npm:^3.0.0": + version: 3.0.0 + resolution: "has-flag@npm:3.0.0" + checksum: 10c0/1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473 + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.2.0 + resolution: "http-cache-semantics@npm:4.2.0" + checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" + dependencies: + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^7.0.1": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:4" + checksum: 10c0/f729219bc735edb621fa30e6e84e60ee5d00802b8247aac0d7b79b0bd6d4b3294737a337b93b86a0bd9e68099d031858a39260c976dc14cdbba238ba1f8779ac + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + languageName: node + linkType: hard + +"ignore-by-default@npm:^1.0.1": + version: 1.0.1 + resolution: "ignore-by-default@npm:1.0.1" + checksum: 10c0/9ab6e70e80f7cc12735def7ecb5527cfa56ab4e1152cd64d294522827f2dcf1f6d85531241537dc3713544e88dd888f65cb3c49c7b2cddb9009087c75274e533 + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + languageName: node + linkType: hard + +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc + languageName: node + linkType: hard + +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + languageName: node + linkType: hard + +"is-extglob@npm:^2.1.1": + version: 2.1.1 + resolution: "is-extglob@npm:2.1.1" + checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + languageName: node + linkType: hard + +"is-glob@npm:^4.0.1, is-glob@npm:~4.0.1": + version: 4.0.3 + resolution: "is-glob@npm:4.0.3" + dependencies: + is-extglob: "npm:^2.1.1" + checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + languageName: node + linkType: hard + +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + languageName: node + linkType: hard + +"jackspeak@npm:^3.1.2": + version: 3.4.3 + resolution: "jackspeak@npm:3.4.3" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 10c0/6acc10d139eaefdbe04d2f679e6191b3abf073f111edf10b1de5302c97ec93fffeb2fdd8681ed17f16268aa9dd4f8c588ed9d1d3bffbbfa6e8bf897cbb3149b9 + languageName: node + linkType: hard + +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 + languageName: node + linkType: hard + +"lodash@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c + languageName: node + linkType: hard + +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.4.3 + resolution: "lru-cache@npm:10.4.3" + checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb + languageName: node + linkType: hard + +"luxon@npm:^3.6.1": + version: 3.6.1 + resolution: "luxon@npm:3.6.1" + checksum: 10c0/906d57a9dc4d1de9383f2e9223e378c298607c1b4d17b6657b836a3cd120feb1c1de3b5d06d846a3417e1ca764de8476e8c23b3cd4083b5cdb870adcb06a99d5 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^14.0.3": + version: 14.0.3 + resolution: "make-fetch-happen@npm:14.0.3" + dependencies: + "@npmcli/agent": "npm:^3.0.0" + cacache: "npm:^19.0.1" + http-cache-semantics: "npm:^4.1.1" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^4.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^1.0.0" + proc-log: "npm:^5.0.0" + promise-retry: "npm:^2.0.1" + ssri: "npm:^12.0.0" + checksum: 10c0/c40efb5e5296e7feb8e37155bde8eb70bc57d731b1f7d90e35a092fde403d7697c56fb49334d92d330d6f1ca29a98142036d6480a12681133a0a1453164cb2f0 + languageName: node + linkType: hard + +"minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + languageName: node + linkType: hard + +"minimatch@npm:^9.0.4": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed + languageName: node + linkType: hard + +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + languageName: node + linkType: hard + +"minipass-fetch@npm:^4.0.0": + version: 4.0.1 + resolution: "minipass-fetch@npm:4.0.1" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^3.0.1" + dependenciesMeta: + encoding: + optional: true + checksum: 10c0/a3147b2efe8e078c9bf9d024a0059339c5a09c5b1dded6900a219c218cc8b1b78510b62dae556b507304af226b18c3f1aeb1d48660283602d5b6586c399eed5c + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": + version: 7.1.2 + resolution: "minipass@npm:7.1.2" + checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 + languageName: node + linkType: hard + +"minizlib@npm:^3.0.1": + version: 3.0.2 + resolution: "minizlib@npm:3.0.2" + dependencies: + minipass: "npm:^7.1.2" + checksum: 10c0/9f3bd35e41d40d02469cb30470c55ccc21cae0db40e08d1d0b1dff01cc8cc89a6f78e9c5d2b7c844e485ec0a8abc2238111213fdc5b2038e6d1012eacf316f78 + languageName: node + linkType: hard + +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 10c0/9f2b975e9246351f5e3a40dcfac99fcd0baa31fbfab615fe059fb11e51f10e4803c63de1f384c54d656e4db31d000e4767e9ef076a22e12a641357602e31d57d + languageName: node + linkType: hard + +"ms@npm:^2.1.3": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + languageName: node + linkType: hard + +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 11.2.0 + resolution: "node-gyp@npm:11.2.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^14.0.3" + nopt: "npm:^8.0.0" + proc-log: "npm:^5.0.0" + semver: "npm:^7.3.5" + tar: "npm:^7.4.3" + tinyglobby: "npm:^0.2.12" + which: "npm:^5.0.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 10c0/bd8d8c76b06be761239b0c8680f655f6a6e90b48e44d43415b11c16f7e8c15be346fba0cbf71588c7cdfb52c419d928a7d3db353afc1d952d19756237d8f10b9 + languageName: node + linkType: hard + +"nodemon@npm:^3.1.9": + version: 3.1.10 + resolution: "nodemon@npm:3.1.10" + dependencies: + chokidar: "npm:^3.5.2" + debug: "npm:^4" + ignore-by-default: "npm:^1.0.1" + minimatch: "npm:^3.1.2" + pstree.remy: "npm:^1.1.8" + semver: "npm:^7.5.3" + simple-update-notifier: "npm:^2.0.0" + supports-color: "npm:^5.5.0" + touch: "npm:^3.1.0" + undefsafe: "npm:^2.0.5" + bin: + nodemon: bin/nodemon.js + checksum: 10c0/95b64d647f2c22e85e375b250517b0a4b32c2d2392ad898444e331f70d6b1ab43b17f53a8a1d68d5879ab8401fc6cd6e26f0d2a8736240984f6b5a8435b407c0 + languageName: node + linkType: hard + +"nopt@npm:^8.0.0": + version: 8.1.0 + resolution: "nopt@npm:8.1.0" + dependencies: + abbrev: "npm:^3.0.0" + bin: + nopt: bin/nopt.js + checksum: 10c0/62e9ea70c7a3eb91d162d2c706b6606c041e4e7b547cbbb48f8b3695af457dd6479904d7ace600856bf923dd8d1ed0696f06195c8c20f02ac87c1da0e1d315ef + languageName: node + linkType: hard + +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": + version: 3.0.0 + resolution: "normalize-path@npm:3.0.0" + checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 + languageName: node + linkType: hard + +"p-map@npm:^7.0.2": + version: 7.0.3 + resolution: "p-map@npm:7.0.3" + checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c + languageName: node + linkType: hard + +"package-json-from-dist@npm:^1.0.0": + version: 1.0.1 + resolution: "package-json-from-dist@npm:1.0.1" + checksum: 10c0/62ba2785eb655fec084a257af34dbe24292ab74516d6aecef97ef72d4897310bc6898f6c85b5cd22770eaa1ce60d55a0230e150fb6a966e3ecd6c511e23d164b + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + languageName: node + linkType: hard + +"path-scurry@npm:^1.11.1": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" + dependencies: + lru-cache: "npm:^10.2.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d + languageName: node + linkType: hard + +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be + languageName: node + linkType: hard + +"picomatch@npm:^4.0.2": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: 10c0/7c51f3ad2bb42c776f49ebf964c644958158be30d0a510efd5a395e8d49cb5acfed5b82c0c5b365523ce18e6ab85013c9ebe574f60305892ec3fa8eee8304ccc + languageName: node + linkType: hard + +"proc-log@npm:^5.0.0": + version: 5.0.0 + resolution: "proc-log@npm:5.0.0" + checksum: 10c0/bbe5edb944b0ad63387a1d5b1911ae93e05ce8d0f60de1035b218cdcceedfe39dbd2c697853355b70f1a090f8f58fe90da487c85216bf9671f9499d1a897e9e3 + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + languageName: node + linkType: hard + +"pstree.remy@npm:^1.1.8": + version: 1.1.8 + resolution: "pstree.remy@npm:1.1.8" + checksum: 10c0/30f78c88ce6393cb3f7834216cb6e282eb83c92ccb227430d4590298ab2811bc4a4745f850a27c5178e79a8f3e316591de0fec87abc19da648c2b3c6eb766d14 + languageName: node + linkType: hard + +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + languageName: node + linkType: hard + +"readline-sync@npm:^1.4.10": + version: 1.4.10 + resolution: "readline-sync@npm:1.4.10" + checksum: 10c0/0a4d0fe4ad501f8f005a3c9cbf3cc0ae6ca2ced93e9a1c7c46f226bdfcb6ef5d3f437ae7e9d2e1098ee13524a3739c830e4c8dbc7f543a693eecd293e41093a3 + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + languageName: node + linkType: hard + +"rxjs@npm:^7.8.1": + version: 7.8.2 + resolution: "rxjs@npm:7.8.2" + dependencies: + tslib: "npm:^2.1.0" + checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + languageName: node + linkType: hard + +"semver@npm:^7.3.5, semver@npm:^7.5.3": + version: 7.7.2 + resolution: "semver@npm:7.7.2" + bin: + semver: bin/semver.js + checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea + languageName: node + linkType: hard + +"shadow-cljs-jar@npm:1.3.4": + version: 1.3.4 + resolution: "shadow-cljs-jar@npm:1.3.4" + checksum: 10c0/c5548bb5f2bda5e0a90df6f42e4ec3a07ed4c72cdebb87619e8d9a2167bb3d4b60d6f6a305a3e15cbfb379d5fdbe2a989a0e7059b667cfb3911bc198a4489e94 + languageName: node + linkType: hard + +"shadow-cljs@npm:3.0.5": + version: 3.0.5 + resolution: "shadow-cljs@npm:3.0.5" + dependencies: + readline-sync: "npm:^1.4.10" + shadow-cljs-jar: "npm:1.3.4" + source-map-support: "npm:^0.5.21" + which: "npm:^5.0.0" + ws: "npm:^8.18.1" + bin: + shadow-cljs: cli/runner.js + checksum: 10c0/2c5f3976f7bec16b7fb9fbba5d4a7581e0d0157384a470ce0670120f02cfe6b9c7183102133e0e9b300cbe318e9a3b6001309f96840999ca2814d39fc83c23e8 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + languageName: node + linkType: hard + +"shell-quote@npm:^1.8.1": + version: 1.8.2 + resolution: "shell-quote@npm:1.8.2" + checksum: 10c0/85fdd44f2ad76e723d34eb72c753f04d847ab64e9f1f10677e3f518d0e5b0752a176fd805297b30bb8c3a1556ebe6e77d2288dbd7b7b0110c7e941e9e9c20ce1 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + languageName: node + linkType: hard + +"simple-update-notifier@npm:^2.0.0": + version: 2.0.0 + resolution: "simple-update-notifier@npm:2.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10c0/2a00bd03bfbcbf8a737c47ab230d7920f8bfb92d1159d421bdd194479f6d01ebc995d13fbe13d45dace23066a78a3dc6642999b4e3b38b847e6664191575b20c + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.5 + resolution: "socks-proxy-agent@npm:8.0.5" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + socks: "npm:^2.8.3" + checksum: 10c0/5d2c6cecba6821389aabf18728325730504bf9bb1d9e342e7987a5d13badd7a98838cc9a55b8ed3cb866ad37cc23e1086f09c4d72d93105ce9dfe76330e9d2a6 + languageName: node + linkType: hard + +"socks@npm:^2.8.3": + version: 2.8.4 + resolution: "socks@npm:2.8.4" + dependencies: + ip-address: "npm:^9.0.5" + smart-buffer: "npm:^4.2.0" + checksum: 10c0/00c3271e233ccf1fb83a3dd2060b94cc37817e0f797a93c560b9a7a86c4a0ec2961fb31263bdd24a3c28945e24868b5f063cd98744171d9e942c513454b50ae5 + languageName: node + linkType: hard + +"source-map-support@npm:^0.5.21": + version: 0.5.21 + resolution: "source-map-support@npm:0.5.21" + dependencies: + buffer-from: "npm:^1.0.0" + source-map: "npm:^0.6.0" + checksum: 10c0/9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d + languageName: node + linkType: hard + +"source-map@npm:^0.6.0": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec + languageName: node + linkType: hard + +"ssri@npm:^12.0.0": + version: 12.0.0 + resolution: "ssri@npm:12.0.0" + dependencies: + minipass: "npm:^7.0.3" + checksum: 10c0/caddd5f544b2006e88fa6b0124d8d7b28208b83c72d7672d5ade44d794525d23b540f3396108c4eb9280dcb7c01f0bef50682f5b4b2c34291f7c5e211fd1417d + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + languageName: node + linkType: hard + +"supports-color@npm:^5.5.0": + version: 5.5.0 + resolution: "supports-color@npm:5.5.0" + dependencies: + has-flag: "npm:^3.0.0" + checksum: 10c0/6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05 + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10c0/afb4c88521b8b136b5f5f95160c98dee7243dc79d5432db7efc27efb219385bbc7d9427398e43dd6cc730a0f87d5085ce1652af7efbe391327bc0a7d0f7fc124 + languageName: node + linkType: hard + +"supports-color@npm:^8.1.1": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89 + languageName: node + linkType: hard + +"tar@npm:^7.4.3": + version: 7.4.3 + resolution: "tar@npm:7.4.3" + dependencies: + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.0.1" + mkdirp: "npm:^3.0.1" + yallist: "npm:^5.0.0" + checksum: 10c0/d4679609bb2a9b48eeaf84632b6d844128d2412b95b6de07d53d8ee8baf4ca0857c9331dfa510390a0727b550fd543d4d1a10995ad86cdf078423fbb8d99831d + languageName: node + linkType: hard + +"tinyglobby@npm:^0.2.12": + version: 0.2.13 + resolution: "tinyglobby@npm:0.2.13" + dependencies: + fdir: "npm:^6.4.4" + picomatch: "npm:^4.0.2" + checksum: 10c0/ef07dfaa7b26936601d3f6d999f7928a4d1c6234c5eb36896bb88681947c0d459b7ebe797022400e555fe4b894db06e922b95d0ce60cb05fd827a0a66326b18c + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 + languageName: node + linkType: hard + +"touch@npm:^3.1.0": + version: 3.1.1 + resolution: "touch@npm:3.1.1" + bin: + nodetouch: bin/nodetouch.js + checksum: 10c0/d2e4d269a42c846a22a29065b9af0b263de58effc85a1764bb7a2e8fc4b47700e9e2fcbd7eb1f5bffbb7c73d860f93600cef282b93ddac8f0b62321cb498b36e + languageName: node + linkType: hard + +"tree-kill@npm:^1.2.2": + version: 1.2.2 + resolution: "tree-kill@npm:1.2.2" + bin: + tree-kill: cli.js + checksum: 10c0/7b1b7c7f17608a8f8d20a162e7957ac1ef6cd1636db1aba92f4e072dc31818c2ff0efac1e3d91064ede67ed5dc57c565420531a8134090a12ac10cf792ab14d2 + languageName: node + linkType: hard + +"tslib@npm:^2.1.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 + languageName: node + linkType: hard + +"undefsafe@npm:^2.0.5": + version: 2.0.5 + resolution: "undefsafe@npm:2.0.5" + checksum: 10c0/96c0466a5fbf395917974a921d5d4eee67bca4b30d3a31ce7e621e0228c479cf893e783a109af6e14329b52fe2f0cb4108665fad2b87b0018c0df6ac771261d5 + languageName: node + linkType: hard + +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 + languageName: node + linkType: hard + +"unique-filename@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-filename@npm:4.0.0" + dependencies: + unique-slug: "npm:^5.0.0" + checksum: 10c0/38ae681cceb1408ea0587b6b01e29b00eee3c84baee1e41fd5c16b9ed443b80fba90c40e0ba69627e30855570a34ba8b06702d4a35035d4b5e198bf5a64c9ddc + languageName: node + linkType: hard + +"unique-slug@npm:^5.0.0": + version: 5.0.0 + resolution: "unique-slug@npm:5.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 10c0/d324c5a44887bd7e105ce800fcf7533d43f29c48757ac410afd42975de82cc38ea2035c0483f4de82d186691bf3208ef35c644f73aa2b1b20b8e651be5afd293 + languageName: node + linkType: hard + +"which@npm:^2.0.1": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + languageName: node + linkType: hard + +"which@npm:^5.0.0": + version: 5.0.0 + resolution: "which@npm:5.0.0" + dependencies: + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 10c0/e556e4cd8b7dbf5df52408c9a9dd5ac6518c8c5267c8953f5b0564073c66ed5bf9503b14d876d0e9c7844d4db9725fb0dcf45d6e911e17e26ab363dc3965ae7b + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + languageName: node + linkType: hard + +"ws@npm:^8.18.1": + version: 8.18.2 + resolution: "ws@npm:8.18.2" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/4b50f67931b8c6943c893f59c524f0e4905bbd183016cfb0f2b8653aa7f28dad4e456b9d99d285bbb67cca4fedd9ce90dfdfaa82b898a11414ebd66ee99141e4 + languageName: node + linkType: hard + +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249 + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 + languageName: node + linkType: hard + +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 + languageName: node + linkType: hard + +"yargs@npm:^17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 + languageName: node + linkType: hard diff --git a/package.json b/package.json index 1ef10821c..2c2b65f98 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",