mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 19:59:54 -04:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d2c37e52c | ||
|
|
f7fd3e1cd5 | ||
|
|
f7bdc9786c | ||
|
|
88a52d1098 | ||
|
|
2318866f8d | ||
|
|
4da6499197 | ||
|
|
7ac61e0597 | ||
|
|
509f5395cb |
No files matched your search
@@ -0,0 +1,58 @@
|
||||
name: "CI: Exporter"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'exporter/**'
|
||||
- 'common/**'
|
||||
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
paths:
|
||||
- 'exporter/**'
|
||||
- 'common/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-exporter:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Exporter Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint
|
||||
working-directory: ./exporter
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt:clj
|
||||
pnpm run lint:clj
|
||||
|
||||
- name: Tests
|
||||
working-directory: ./exporter
|
||||
run: |
|
||||
./scripts/test
|
||||
@@ -212,6 +212,37 @@ superseded it:
|
||||
|
||||
Replace the reference in the changelog entry with the correct merged PR number.
|
||||
|
||||
### 5b. Security advisory (GHSA) entries
|
||||
|
||||
Security advisories fixed in a release are documented in the changelog even
|
||||
though they are **neither milestone issues nor PRs**. The GHSA ID and its
|
||||
description are supplied by the user or the release notes — they never come
|
||||
from the milestone fetch in step 2.
|
||||
|
||||
**Format** (matches the existing precedent in `CHANGES.md`, e.g. the
|
||||
`create-font-variant` arbitrary file read advisory):
|
||||
|
||||
```markdown
|
||||
- Fix <user-facing description> (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX)
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Place the entry under `### :bug: Bugs fixed`, with **no issue or PR link** —
|
||||
only the advisory URL.
|
||||
- The advisory may be **draft/unpublished** at changelog time (the URL 404s
|
||||
publicly). Do **not** web-fetch or verify the URL, and do **not** drop the
|
||||
entry because of that. Rely on the GHSA ID provided by the user.
|
||||
- Derive the description from the supplied advisory title, imperative mood and
|
||||
user-facing (e.g. `Fix command injection in SVG exporter via legacy fill-color`).
|
||||
- These entries are **invisible to the automation**: they are not returned by
|
||||
`gh.py issues`, not matched by `--compare` (step 3), not part of the PR
|
||||
cross-reference (step 10), and not scanned by the anomaly-report regexes
|
||||
(step 11, which only match `issues/` and `pull/` links). Add them manually.
|
||||
- During pre-flight checks (step 6a) apply only the **backport/duplicate**
|
||||
check: if the same GHSA already appears in an earlier version section, remove
|
||||
it from the current section. Their absence from milestone cross-references
|
||||
is expected, not an anomaly.
|
||||
|
||||
### 6. Read the current CHANGES.md
|
||||
|
||||
Read the top of `CHANGES.md` to understand the existing format and find the
|
||||
@@ -400,6 +431,8 @@ if closed:
|
||||
- ✅ Every merged milestone PR is either in the changelog or excluded by label
|
||||
- ✅ PR and issue counts are internally consistent
|
||||
- ✅ No false-positive PR-to-issue associations
|
||||
- ✅ Advisory (GHSA) entries are not milestone PRs — their absence from the
|
||||
cross-reference is intentional (see step 5b)
|
||||
|
||||
## Version section template
|
||||
|
||||
@@ -410,8 +443,12 @@ if closed:
|
||||
|
||||
- <fix description> [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
|
||||
- <fix description> (by @contributor) [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
|
||||
- <fix description> (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX)
|
||||
```
|
||||
|
||||
Advisory (GHSA) entries have no issue or PR link — just the advisory URL. See
|
||||
step 5b.
|
||||
|
||||
### 11. Generate anomaly report and save to CHANGES-ISSUES.md
|
||||
|
||||
After all edits and cross-referencing are complete, generate a structured
|
||||
@@ -732,6 +769,14 @@ self-contained and clickable in any Markdown viewer.
|
||||
Taiga description text or by searching GitHub PRs that reference the Taiga
|
||||
URL. Replace the Taiga reference with the GitHub issue link and add the PR
|
||||
reference if applicable.
|
||||
- **Security advisory (GHSA) entries.** Advisories fixed in the release are
|
||||
listed under `### :bug: Bugs fixed` with the advisory URL and **no issue or
|
||||
PR link**, even though they are not in the milestone. The GHSA ID and
|
||||
description come from the user — do **not** fetch or verify the URL, and do
|
||||
not drop a draft (unpublished) advisory. Precedent:
|
||||
`- Fix arbitrary file read security issue on create-font-variant rpc method
|
||||
(https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)`.
|
||||
See step 5b.
|
||||
- **Re-fetch before editing.** Milestones can change — always re-fetch issues
|
||||
before making edits, don't rely on cached data.
|
||||
- **Use `scripts/gh.py`.** Prefer the helper script over raw `gh api` calls for
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
## Layout and commands
|
||||
|
||||
- Source: `exporter/src/`; config: `deps.edn`, `shadow-cljs.edn`, `package.json`; runtime helpers/assets: `vendor/`, `scripts/`.
|
||||
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
|
||||
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; test bundle `pnpm run build:test`; tests `pnpm run test` or `pnpm run test:quiet`; lint `pnpm run lint:clj`; format check/fix `pnpm run check-fmt:clj` / `pnpm run fmt:clj`.
|
||||
- Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`.
|
||||
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
||||
- Exporter test conventions and CI: `mem:exporter/testing`.
|
||||
|
||||
## HTTP and browser pool
|
||||
|
||||
@@ -31,4 +32,4 @@
|
||||
- WebP is produced by taking a PNG screenshot and converting it with ImageMagick.
|
||||
- SVG export rasterizes text foreignObjects to PNG, converts through PPM/color masks/potrace, and reassembles SVG paths. It also replaces non-breaking spaces for SVG compatibility and drops empty defs/paths.
|
||||
- PDF export injects `@page` sizing through raw browser `evaluate` JavaScript; that code cannot rely on CLJS runtime helpers.
|
||||
- Temporary resources schedule local deletion, then uploads POST to `/api/management/methods/upload-tempfile` with `X-Shared-Key: exporter <management-key>` and Bearer auth.
|
||||
- Temporary resources schedule local deletion, then uploads POST to `/api/management/methods/upload-tempfile` with `X-Shared-Key: exporter <management-key>` and Bearer auth.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Exporter Testing
|
||||
|
||||
- READ `mem:testing` first.
|
||||
- Tests use `cljs.test` and live under `exporter/test/exporter_tests/`.
|
||||
- Register every test namespace in `exporter-tests.runner`.
|
||||
- From `exporter/`: `pnpm run build:test` builds the Node test bundle without running tests.
|
||||
- From `exporter/`: `pnpm run test` builds and runs tests with full output.
|
||||
- From `exporter/`: `pnpm run test:quiet` builds and runs tests with reduced build output.
|
||||
- After `build:test`, reuse the compiled bundle with `node target/tests/test.js`.
|
||||
- For iterative focused runs, build once and reuse the compiled bundle.
|
||||
- Focus a test namespace with `node target/tests/test.js --focus exporter-tests.renderer-svg-test`.
|
||||
- Focus a test var with `node target/tests/test.js --focus exporter-tests.renderer-svg-test/creates-the-correct-gradient-element`.
|
||||
- Set app log level by appending `--log-level warn` (or `trace|debug|info|warn|error`).
|
||||
- `test:quiet` accepts forwarded options but rebuilds the bundle; prefer the direct runner after `build:test` for focused runs.
|
||||
- From `exporter/`: `pnpm run check-fmt:clj` checks ClojureScript formatting.
|
||||
- From `exporter/`: `pnpm run lint:clj` runs ClojureScript linting.
|
||||
+26
-3
@@ -1,15 +1,38 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.17.1 (Unreleased)
|
||||
## 2.17.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix linear gradients in SVG text exports being emitted as radial gradients [#5972](https://github.com/penpot/penpot/issues/5972) (PR: [#11272](https://github.com/penpot/penpot/pull/11272))
|
||||
- Fix typography token becoming detached when editing text content [#11362](https://github.com/penpot/penpot/issues/11362) (PR: [#11366](https://github.com/penpot/penpot/pull/11366))
|
||||
- Fix command injection in SVG exporter via legacy fill-color (https://github.com/penpot/penpot/security/advisories/GHSA-4f36-m4hj-cv86)
|
||||
|
||||
## 2.17.1
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix overrides lost after switching component variant [#10588](https://github.com/penpot/penpot/issues/10588) (PR: [#10619](https://github.com/penpot/penpot/pull/10619))
|
||||
- Fix malformed get-font-variants request when team-id is missing from dashboard URL [#10644](https://github.com/penpot/penpot/issues/10644) (PR: [#10645](https://github.com/penpot/penpot/pull/10645))
|
||||
- Fix malformed get-profiles-for-file-comments request when file-id is missing from workspace URL [#10652](https://github.com/penpot/penpot/issues/10652) (PR: [#10655](https://github.com/penpot/penpot/pull/10655))
|
||||
- Fix workspace crash when holding an arrow key on a selection due to excessive re-renders [#10726](https://github.com/penpot/penpot/issues/10726) (PR: [#10736](https://github.com/penpot/penpot/pull/10736))
|
||||
- Fix asset download failing with S3 auth conflict when using access token [#10776](https://github.com/penpot/penpot/issues/10776) (PR: [#10777](https://github.com/penpot/penpot/pull/10777))
|
||||
- Fix internal error when dragging inner layout with Boolean operations [#10647](https://github.com/penpot/penpot/issues/10647) (PR: [#10778](https://github.com/penpot/penpot/pull/10778))
|
||||
- Fix frontend throwing raw TypeError on undefined .getData receivers across import, paste, drag, and text editor paths [#10709](https://github.com/penpot/penpot/issues/10709) (PR: [#10718](https://github.com/penpot/penpot/pull/10718))
|
||||
- Fix workspace crash with 'can't access dead object' in Firefox when navigating between pages [#10719](https://github.com/penpot/penpot/issues/10719) (PR: [#10721](https://github.com/penpot/penpot/pull/10721))
|
||||
- Fix workspace crash when holding an arrow key on a selection due to excessive re-renders [#10726](https://github.com/penpot/penpot/issues/10726) (PR: [#10736](https://github.com/penpot/penpot/pull/10736))
|
||||
- Fix dashboard sidebar throwing removeChild NotFoundError during rapid keyboard navigation [#10714](https://github.com/penpot/penpot/issues/10714) (PR: [#10715](https://github.com/penpot/penpot/pull/10715))
|
||||
- Fix asset download failing with S3 auth conflict when using access token [#10776](https://github.com/penpot/penpot/issues/10776) (PR: [#10777](https://github.com/penpot/penpot/pull/10777))
|
||||
- Fix import worker crashing when importing non-Penpot zip files [#10781](https://github.com/penpot/penpot/issues/10781) (PR: [#10782](https://github.com/penpot/penpot/pull/10782))
|
||||
- Fix viewer crash with WASM panic when opening URL with page-id [#10800](https://github.com/penpot/penpot/issues/10800) (PR: [#10805](https://github.com/penpot/penpot/pull/10805))
|
||||
- Fix backend returning 500 when JSON request body has unrecognized escape sequence [#10804](https://github.com/penpot/penpot/issues/10804) (PR: [#10808](https://github.com/penpot/penpot/pull/10808))
|
||||
- Fix color picker eyedropper crashing when viewport is unmounted during pointer move [#10811](https://github.com/penpot/penpot/issues/10811) (PR: [#10812](https://github.com/penpot/penpot/pull/10812))
|
||||
- Fix flex layout crash when dragging shapes with missing bounds [#10843](https://github.com/penpot/penpot/issues/10843) (PR: [#10845](https://github.com/penpot/penpot/pull/10845))
|
||||
- Fix export failing when shape has blank layer name [#10849](https://github.com/penpot/penpot/issues/10849) (PR: [#10852](https://github.com/penpot/penpot/pull/10852))
|
||||
- Fix area selection (marquee) being aborted by select-shapes interrupt [#10872](https://github.com/penpot/penpot/issues/10872) (PR: [#10870](https://github.com/penpot/penpot/pull/10870))
|
||||
- Fix gradient editor sending invalid stop offset when clicking outside gradient line [#10879](https://github.com/penpot/penpot/issues/10879) (PR: [#10881](https://github.com/penpot/penpot/pull/10881))
|
||||
- Fix audit event validation failing when error reports contain string profile-id and missing token context [#10897](https://github.com/penpot/penpot/issues/10897) (PR: [#10898](https://github.com/penpot/penpot/pull/10898))
|
||||
- Fix MCP tool call timeout being too low for some operations [#10953](https://github.com/penpot/penpot/issues/10953) (PR: [#10967](https://github.com/penpot/penpot/pull/10967))
|
||||
- Fix MCP requests running into timeouts after leaving a file in Penpot [#10958](https://github.com/penpot/penpot/issues/10958) (PR: [#10967](https://github.com/penpot/penpot/pull/10967))
|
||||
- Fix duplicate WebSocket MCP connection attempts deregistering the original connection's routing entries [#10961](https://github.com/penpot/penpot/issues/10961) (PR: [#10967](https://github.com/penpot/penpot/pull/10967))
|
||||
|
||||
## 2.17.0
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.path-names :as cpn]
|
||||
[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.pages-list :as ctpl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
(log/set-level! :debug)
|
||||
@@ -35,7 +37,7 @@
|
||||
(assoc :width 0.01)
|
||||
(assoc :height 0.01)
|
||||
(cts/setup-rect)))]
|
||||
(log/dbg :hint "repairing shape :invalid-geometry" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :invalid-geometry" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -48,7 +50,7 @@
|
||||
(log/debug :hint " -> set to " :parent-id uuid/zero)
|
||||
(assoc shape :parent-id uuid/zero))]
|
||||
|
||||
(log/dbg :hint "repairing shape :parent-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :parent-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -61,7 +63,7 @@
|
||||
(log/debug :hint " -> add children to" :parent-id (:id parent-shape))
|
||||
(update parent-shape :shapes conj (:id shape)))]
|
||||
|
||||
(log/dbg :hint "repairing shape :child-not-in-parent" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :child-not-in-parent" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:parent-id shape)] repair-shape))))
|
||||
@@ -74,7 +76,7 @@
|
||||
(log/debug :hint " -> remove duplicated children")
|
||||
(update shape :shapes distinct))]
|
||||
|
||||
(log/dbg :hint "repairing shape :duplicated-children" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :duplicated-children" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -86,14 +88,14 @@
|
||||
(log/debug :hint " -> remove child" :child-id (:child-id args))
|
||||
(update parent-shape :shapes (fn [shapes]
|
||||
(d/removev #(= (:child-id args) %) shapes))))]
|
||||
(log/dbg :hint "repairing shape :child-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :child-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :invalid-parent
|
||||
[_ {:keys [shape page-id args] :as error} file-data _]
|
||||
(log/dbg :hint "repairing shape :invalid-parent" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :invalid-parent" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/change-parent (:parent-id args) [shape] nil {:allow-altering-copies true})))
|
||||
@@ -109,7 +111,7 @@
|
||||
(log/debug :hint " -> set to " :frame-id frame-id)
|
||||
(assoc shape :frame-id frame-id)))]
|
||||
|
||||
(log/dbg :hint "repairing shape :frame-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :frame-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -125,7 +127,7 @@
|
||||
(log/debug :hint " -> set to " :frame-id frame-id)
|
||||
(assoc shape :frame-id frame-id)))]
|
||||
|
||||
(log/dbg :hint "repairing shape :invalid-frame" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :invalid-frame" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -138,7 +140,7 @@
|
||||
(log/debug :hint " -> set :main-instance")
|
||||
(assoc shape :main-instance true))]
|
||||
|
||||
(log/dbg :hint "repairing shape :component-not-main" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :component-not-main" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -155,7 +157,7 @@
|
||||
;; (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
|
||||
;; shape)]
|
||||
|
||||
(log/dbg :hint "repairing shape :component-main-external" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :component-main-external" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -174,7 +176,7 @@
|
||||
;; (log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
|
||||
;; shape)]
|
||||
|
||||
(log/dbg :hint "repairing shape :component-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :component-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes shape-ids repair-shape))))
|
||||
@@ -194,7 +196,7 @@
|
||||
(log/debug :hint " -> detach shape" :shape-id (:id shape))
|
||||
(ctk/detach-shape shape))]
|
||||
|
||||
(log/dbg :hint "repairing shape :invalid-main-instance-id" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :invalid-main-instance-id" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(if (and (some? component) (not (:deleted component)))
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-library-data file-data)
|
||||
@@ -211,7 +213,7 @@
|
||||
;; Assign main instance in the component to current shape
|
||||
(log/debug :hint " -> assign main-instance-page" :component-id (:id component))
|
||||
(assoc component :main-instance-page page-id))]
|
||||
(log/dbg :hint "repairing shape :invalid-main-instance-page" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :invalid-main-instance-page" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:component-id shape) repair-component))))
|
||||
@@ -224,7 +226,7 @@
|
||||
(log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
|
||||
shape)]
|
||||
|
||||
(log/dbg :hint "repairing shape :invalid-main-instance" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :invalid-main-instance" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -237,7 +239,7 @@
|
||||
(log/debug :hint " -> unset :main-instance")
|
||||
(dissoc shape :main-instance))]
|
||||
|
||||
(log/dbg :hint "repairing shape :component-main" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :component-main" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -250,7 +252,7 @@
|
||||
(log/debug :hint " -> set :component-root")
|
||||
(assoc shape :component-root true))]
|
||||
|
||||
(log/dbg :hint "repairing shape :should-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :should-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -263,7 +265,7 @@
|
||||
(log/debug :hint " -> unset :component-root")
|
||||
(dissoc shape :component-root))]
|
||||
|
||||
(log/dbg :hint "repairing shape :should-not-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :should-not-be-component-root" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -310,7 +312,7 @@
|
||||
|
||||
;; If the shape still refers to the remote component, try to find the corresponding near one
|
||||
;; and link to it. If not, detach the shape.
|
||||
(log/dbg :hint "repairing shape :ref-shape-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :ref-shape-not-found" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(if (some? matching-shape)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
@@ -329,7 +331,7 @@
|
||||
(log/debug :hint " -> unhead shape")
|
||||
(ctk/unhead-shape shape))]
|
||||
|
||||
(log/dbg :hint "repairing shape :shape-ref-is-not-head" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :shape-ref-is-not-head" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -354,7 +356,7 @@
|
||||
(nil? (:component-file args))
|
||||
(dissoc :component-file)))]
|
||||
|
||||
(log/dbg :hint "repairing shape :component-id-mismatch" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :component-id-mismatch" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -367,7 +369,7 @@
|
||||
(log/debug :hint " -> reroot shape")
|
||||
(ctk/rehead-shape shape (:component-file args) (:component-id args)))]
|
||||
|
||||
(log/dbg :hint "repairing shape :shape-ref-is-head" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :shape-ref-is-head" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -396,7 +398,7 @@
|
||||
(assoc acc k v)))
|
||||
{}
|
||||
objects)))))]
|
||||
(log/dbg :hint "repairing component :shape-ref-cycle" :id (:id shape) :name (:name shape))
|
||||
(log/debug :hint "repairing component :shape-ref-cycle" :id (:id shape) :name (:name shape))
|
||||
(-> (pcb/empty-changes nil nil)
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:id shape) repair-component))))
|
||||
@@ -409,7 +411,7 @@
|
||||
(log/debug :hint " -> unset :shape-ref")
|
||||
(dissoc shape :shape-ref))]
|
||||
|
||||
(log/dbg :hint "repairing shape :shape-ref-in-main" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :shape-ref-in-main" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -422,7 +424,7 @@
|
||||
(log/debug :hint " -> unset :component-root")
|
||||
(dissoc shape :component-root))]
|
||||
|
||||
(log/dbg :hint "repairing shape :root-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :root-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -435,7 +437,7 @@
|
||||
(log/debug :hint " -> set :component-root")
|
||||
(assoc shape :component-root true))]
|
||||
|
||||
(log/dbg :hint "repairing shape :nested-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :nested-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape)
|
||||
@@ -449,7 +451,7 @@
|
||||
(log/debug :hint " -> unset :component-root")
|
||||
(dissoc shape :component-root))]
|
||||
|
||||
(log/dbg :hint "repairing shape :root-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :root-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -462,7 +464,7 @@
|
||||
(log/debug :hint " -> set :component-root")
|
||||
(assoc shape :component-root true))]
|
||||
|
||||
(log/dbg :hint "repairing shape :nested-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :nested-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -475,7 +477,7 @@
|
||||
(log/debug :hint " -> detach shape" :shape-id (:id shape))
|
||||
(ctk/detach-shape shape))]
|
||||
|
||||
(log/dbg :hint "repairing shape :not-head-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :not-head-main-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -488,7 +490,7 @@
|
||||
(log/debug :hint " -> detach shape" :shape-id (:id shape))
|
||||
(ctk/detach-shape shape))]
|
||||
|
||||
(log/dbg :hint "repairing shape :not-head-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :not-head-copy-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -501,7 +503,7 @@
|
||||
(log/warn :hint " -> CANNOT REPAIR THIS AUTOMATICALLY.")
|
||||
shape)]
|
||||
|
||||
(log/dbg :hint "repairing shape :not-component-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :not-component-not-allowed" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -520,7 +522,7 @@
|
||||
:r3 0
|
||||
:r4 0))]
|
||||
|
||||
(log/dbg :hint "repairing shape :instance-head-not-frame" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :instance-head-not-frame" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -538,7 +540,7 @@
|
||||
(log/debug :hint " -> remove :objects")
|
||||
(dissoc component :objects))))]
|
||||
|
||||
(log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id component) :name (:name component))
|
||||
(log/debug :hint "repairing component :component-nil-objects-not-allowed" :id (:id component) :name (:name component))
|
||||
(-> (pcb/empty-changes nil)
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:id component) repair-component))))
|
||||
@@ -554,7 +556,7 @@
|
||||
(dissoc component :objects))
|
||||
component))]
|
||||
|
||||
(log/dbg :hint "repairing component :non-deleted-component-cannot-have-objects" :id (:id component) :name (:name component))
|
||||
(log/debug :hint "repairing component :non-deleted-component-cannot-have-objects" :id (:id component) :name (:name component))
|
||||
(-> (pcb/empty-changes nil)
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:id component) repair-component))))
|
||||
@@ -567,7 +569,7 @@
|
||||
(log/debug :hint " -> add :content-group to :touched-groups")
|
||||
(update shape :touched ctk/set-touched-group :content-group))]
|
||||
|
||||
(log/dbg :hint "repairing shape :invalid-text-touched" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :invalid-text-touched" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -580,7 +582,7 @@
|
||||
(log/debug :hint " -> remove swap-slot")
|
||||
(ctk/remove-swap-slot shape))]
|
||||
|
||||
(log/dbg :hint "repairing shape :misplaced-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :misplaced-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
@@ -603,13 +605,11 @@
|
||||
(log/debug :hint " -> remove swap-slot" :child-id (:id shape))
|
||||
(ctk/remove-swap-slot shape))]
|
||||
|
||||
(log/dbg :hint "repairing shape :duplicated-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :duplicated-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes (map :id child-with-duplicate) repair-shape))))
|
||||
|
||||
|
||||
|
||||
(defmethod repair-error :component-duplicate-slot
|
||||
[_ {:keys [shape] :as error} file-data _]
|
||||
(let [main-shape (get-in shape [:objects (:main-instance-id shape)])
|
||||
@@ -633,7 +633,7 @@
|
||||
(:objects component))]
|
||||
(assoc component :objects objects)))]
|
||||
|
||||
(log/dbg :hint "repairing component :component-duplicated-slot" :id (:id shape) :name (:name shape))
|
||||
(log/debug :hint "repairing component :component-duplicated-slot" :id (:id shape) :name (:name shape))
|
||||
(-> (pcb/empty-changes nil)
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:id shape) repair-component))))
|
||||
@@ -649,50 +649,139 @@
|
||||
(ctk/set-swap-slot shape slot))
|
||||
shape)))]
|
||||
|
||||
(log/dbg :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :not-a-variant
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
(defmethod repair-error :main-instance-not-a-variant
|
||||
[_ {:keys [shape page-id args]} file-data _]
|
||||
(let [repair-shape
|
||||
(fn [shape]
|
||||
(let [variant-id (:variant-id args)]
|
||||
;; Set the desired variant-id
|
||||
(log/debug :hint (str " -> set variant-id to " variant-id))
|
||||
(assoc shape :variant-id variant-id)))]
|
||||
|
||||
(defmethod repair-error :invalid-variant-id
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
(log/debug :hint "repairing shape :main-instance-not-a-variant" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :main-instance-invalid-variant-id
|
||||
[_ {:keys [shape page-id args]} file-data _]
|
||||
(let [repair-shape
|
||||
(fn [shape]
|
||||
(let [variant-id (:variant-id args)]
|
||||
;; Set the desired variant-id
|
||||
(log/debug :hint (str " -> set variant-id to " variant-id))
|
||||
(assoc shape
|
||||
:variant-id variant-id)))]
|
||||
|
||||
(log/debug :hint "repairing shape :main-instance-invalid-variant-id" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :invalid-variant-properties
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
[_ {:keys [shape page-id args]} file-data _]
|
||||
(let [prop-names (:prop-names args)
|
||||
|
||||
component (get-in file-data [:components (:component-id shape)])
|
||||
prop-values (into {} (map (juxt :name :value)) (:variant-properties component))
|
||||
properties' (mapv (fn [name] {:name name :value (get prop-values name "")}) prop-names)
|
||||
variant-name (ctv/properties-to-name properties')
|
||||
|
||||
repair-component
|
||||
(fn [component]
|
||||
;; Rebuild component properties, removing any extra ones and adding missing ones with empty value
|
||||
(log/debug :hint " -> rebuild properties" :component-id (:id component) :prop-names (str prop-names))
|
||||
(assoc component :variant-properties properties'))
|
||||
|
||||
repair-shape
|
||||
(fn [shape]
|
||||
(log/debug :hint " -> set variant-name" :variant-name variant-name)
|
||||
(assoc shape :variant-name variant-name))]
|
||||
|
||||
(log/debug :hint "repairing shape :invalid-variant-properties" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:component-id shape) repair-component)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :variant-not-main
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
[_ {:keys [shape page-id]} file-data _]
|
||||
(let [page (ctpl/get-page file-data page-id)
|
||||
shape-ids (cfh/get-children-ids-with-self (:objects page) (:id shape))]
|
||||
(log/debug :hint "repairing shape :variant-not-main" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(log/debug :hint " -> delete shapes" :shape-ids shape-ids)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/remove-objects shape-ids))))
|
||||
|
||||
(defmethod repair-error :parent-not-variant
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
[_ {:keys [shape page-id]} file-data _]
|
||||
(let [parent-id (:parent-id shape)
|
||||
repair-fn
|
||||
(fn [parent]
|
||||
(log/debug :hint " -> set :is-variant-container true")
|
||||
(assoc parent :is-variant-container true))]
|
||||
(log/debug :hint "repairing shape :parent-not-variant" :id (:id shape) :name (:name shape) :parent-id parent-id :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [parent-id] repair-fn))))
|
||||
|
||||
(defmethod repair-error :variant-bad-name
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
(defmethod repair-error :variant-main-bad-name
|
||||
[_ {:keys [shape page-id args]} file-data _]
|
||||
(let [repair-fn
|
||||
(fn [shape]
|
||||
(log/debug :hint " -> set :name" :name (:variant-name args))
|
||||
(assoc shape :name (:variant-name args)))]
|
||||
(log/debug :hint "repairing shape :variant-main-bad-name" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-fn))))
|
||||
|
||||
(defmethod repair-error :variant-bad-variant-name
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
(defmethod repair-error :variant-main-bad-variant-name
|
||||
[_ {:keys [shape page-id]} file-data _]
|
||||
(let [component (get-in file-data [:components (:component-id shape)])
|
||||
variant-name (ctv/properties-to-name (:variant-properties component))
|
||||
repair-fn
|
||||
(fn [shape]
|
||||
(log/debug :hint " -> set :variant-name" :variant-name variant-name)
|
||||
(assoc shape :variant-name variant-name))]
|
||||
(log/dbg :hint "repairing shape :variant-main-bad-variant-name" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-fn))))
|
||||
|
||||
(defmethod repair-error :variant-component-bad-name
|
||||
[_ error file _]
|
||||
(log/error :hint "Variant error code, we don't want to auto repair it for now" :code (:code error))
|
||||
file)
|
||||
[_ {:keys [shape page-id args]} file-data _]
|
||||
(let [[path name] (cpn/split-group-name (:variant-container-name args))
|
||||
repair-fn
|
||||
(fn [component]
|
||||
(log/debug :hint " -> set :path and :name" :path path :name name)
|
||||
(assoc component :path path :name name))]
|
||||
(log/dbg :hint "repairing shape :variant-component-bad-name" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:component-id shape) repair-fn))))
|
||||
|
||||
(defmethod repair-error :variant-component-bad-id
|
||||
[_ {:keys [shape page-id args]} file-data _]
|
||||
(let [repair-shape
|
||||
(fn [shape]
|
||||
(let [variant-id (:variant-id args)]
|
||||
;; Set the desired variant-id
|
||||
(log/debug :hint (str " -> set variant-id to " variant-id))
|
||||
(assoc shape
|
||||
:variant-id variant-id)))]
|
||||
|
||||
(log/debug :hint "repairing shape :variant-component-bad-id" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :default
|
||||
[_ error file _]
|
||||
@@ -701,7 +790,7 @@
|
||||
|
||||
(defn repair-file
|
||||
[{:keys [data id] :as file} libraries errors]
|
||||
(log/dbg :hint "repairing file" :id (str id) :errors (count errors))
|
||||
(log/debug :hint "repairing file" :id (str id) :errors (count errors))
|
||||
(let [{:keys [redo-changes]}
|
||||
(reduce (fn [changes error]
|
||||
(pcb/concat-changes changes
|
||||
|
||||
@@ -65,13 +65,13 @@
|
||||
:misplaced-slot
|
||||
:missing-slot
|
||||
:shape-ref-cycle
|
||||
:not-a-variant
|
||||
:invalid-variant-id
|
||||
:main-instance-not-a-variant
|
||||
:main-instance-invalid-variant-id
|
||||
:invalid-variant-properties
|
||||
:variant-not-main
|
||||
:parent-not-variant
|
||||
:variant-bad-name
|
||||
:variant-bad-variant-name
|
||||
:variant-main-bad-name
|
||||
:variant-main-bad-variant-name
|
||||
:variant-component-bad-name
|
||||
:variant-component-bad-id})
|
||||
|
||||
@@ -573,19 +573,23 @@
|
||||
(run! (fn [child-id]
|
||||
(when-let [child (get objects child-id)]
|
||||
(if (not (ctk/is-variant? child))
|
||||
(report-error :not-a-variant
|
||||
(str/ffmt "Shape % should be a variant" (:id child))
|
||||
child file page)
|
||||
(report-error :main-instance-not-a-variant
|
||||
(str/ffmt "Main instance shape % should be a variant" (:id child))
|
||||
child file page
|
||||
:variant-id shape-id)
|
||||
(do
|
||||
(when (not= (:variant-id child) shape-id)
|
||||
(report-error :invalid-variant-id
|
||||
(str/ffmt "Variant % has invalid variant-id %" (:id child) (:variant-id child))
|
||||
child file page))
|
||||
(report-error :main-instance-invalid-variant-id
|
||||
(str/ffmt "Main instance in variant % should have the variant-id of the container but has %" (:id child) (:variant-id child))
|
||||
child file page
|
||||
:variant-id shape-id))
|
||||
(when (not= prop-names (cfv/extract-properties-names child file-data))
|
||||
(report-error :invalid-variant-properties
|
||||
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
|
||||
child file page))))))
|
||||
child file page
|
||||
:prop-names prop-names))))))
|
||||
shapes)))
|
||||
|
||||
(defn- check-variant
|
||||
"Shape is a variant, so
|
||||
-it should be a main component
|
||||
@@ -594,9 +598,9 @@
|
||||
-its name should be the same as its parent's
|
||||
"
|
||||
[shape file page]
|
||||
(let [parent (ctst/get-shape page (:parent-id shape))
|
||||
component (ctkl/get-component (:data file) (:component-id shape) true)
|
||||
name (ctv/properties-to-name (:variant-properties component))]
|
||||
(let [parent (ctst/get-shape page (:parent-id shape))
|
||||
component (ctkl/get-component (:data file) (:component-id shape) true)
|
||||
variant-name (ctv/properties-to-name (:variant-properties component))]
|
||||
(when-not (ctk/main-instance? shape)
|
||||
(report-error :variant-not-main
|
||||
(str/ffmt "Variant % is not a main instance" (:id shape))
|
||||
@@ -605,23 +609,26 @@
|
||||
(report-error :parent-not-variant
|
||||
(str/ffmt "Variant % has an invalid parent" (:id shape))
|
||||
shape file page))
|
||||
|
||||
(when-not (= name (:variant-name shape))
|
||||
(report-error :variant-bad-variant-name
|
||||
(when-not (= variant-name (:variant-name shape))
|
||||
(report-error :variant-main-bad-variant-name
|
||||
(str/ffmt "Variant % has an invalid variant-name" (:id shape))
|
||||
shape file page))
|
||||
shape file page
|
||||
:variant-name variant-name))
|
||||
(when-not (= (:name parent) (:name shape))
|
||||
(report-error :variant-bad-name
|
||||
(str/ffmt "Variant % has an invalid name" (:id shape))
|
||||
shape file page))
|
||||
(report-error :variant-main-bad-name
|
||||
(str/ffmt "Main instance inside variant % has an invalid name" (:id shape))
|
||||
shape file page
|
||||
:variant-name (:name parent)))
|
||||
(when-not (= (:name parent) (cpn/merge-path-item (:path component) (:name component)))
|
||||
(report-error :variant-component-bad-name
|
||||
(str/ffmt "Component % has an invalid name" (:id shape))
|
||||
shape file page))
|
||||
shape file page
|
||||
:variant-container-name (:name parent)))
|
||||
(when-not (= (:variant-id component) (:variant-id shape))
|
||||
(report-error :variant-component-bad-id
|
||||
(str/ffmt "Variant % has adifferent variant-id than its component" (:id shape))
|
||||
shape file page))))
|
||||
shape file page
|
||||
:variant-id (:variant-id component)))))
|
||||
|
||||
(defn- check-shape
|
||||
"Validate referential integrity and semantic coherence of
|
||||
@@ -740,14 +747,15 @@
|
||||
-It should have at least one variant property"
|
||||
[component file]
|
||||
(let [component-page (ctf/get-component-page (:data file) component)
|
||||
main-component (if (:deleted component)
|
||||
main-instance (if (:deleted component)
|
||||
(dm/get-in component [:objects (:main-instance-id component)])
|
||||
(ctst/get-shape component-page (:main-instance-id component)))]
|
||||
(when (and main-component
|
||||
(not (ctk/is-variant? main-component)))
|
||||
(report-error :not-a-variant
|
||||
(str/ffmt "Shape % should be a variant" (:id main-component))
|
||||
main-component file component-page))))
|
||||
(when (and main-instance
|
||||
(not (ctk/is-variant? main-instance)))
|
||||
(report-error :main-instance-not-a-variant
|
||||
(str/ffmt "Main instance shape % should be a variant" (:id main-instance))
|
||||
main-instance file component-page
|
||||
:variant-id (:variant-id component)))))
|
||||
|
||||
(defn- check-main-inside-main
|
||||
[component file]
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
[app.common.types.text :as txt]))
|
||||
|
||||
(defn add-variant
|
||||
"Add a variant component to a file with two variants, each with a root shape.
|
||||
:variant-label [:name Board]
|
||||
{:root2-label} [:name Board] # [Component :component2-label]
|
||||
{:root1-label} [:name Board] # [Component :component1-label]
|
||||
"
|
||||
[file variant-label component1-label root1-label component2-label root2-label
|
||||
& {:keys [variant1-params variant2-params]
|
||||
:or {variant1-params {} variant2-params {}}}]
|
||||
|
||||
@@ -901,8 +901,10 @@
|
||||
(let [shape (get objects shape-id)]
|
||||
(println (str/pad (str (str/repeat " " level)
|
||||
(when (:main-instance shape) "{")
|
||||
(when (:is-variant-container shape) "{{")
|
||||
(:name shape)
|
||||
(when (:main-instance shape) "}")
|
||||
(when (:is-variant-container shape) "}}")
|
||||
(when (seq (:touched shape)) "*")
|
||||
(when show-ids (str/format " %s" (:id shape))))
|
||||
{:length 20
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.flags :as flags]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.color :as clr]
|
||||
[app.common.types.fills :as types.fills]
|
||||
[clojure.set :as set]
|
||||
@@ -217,7 +218,10 @@
|
||||
attributes or other things that may be attached).
|
||||
- Consider nil values, empty strings or empty lists all equal.
|
||||
- Normalize numeric values (legacy) into strings.
|
||||
- No value is equal than the default value."
|
||||
- No value is equal than the default value.
|
||||
- Numeric attrs (e.g. line-height) compare with float tolerance so
|
||||
editor/WASM round-trips like \"1.3333333333333333\" vs \"1.33333\"
|
||||
do not count as a real style change (avoids detaching tokens)."
|
||||
[key value1 value2]
|
||||
(when (text-node-attr? key)
|
||||
(let [default-value (get default-text-attrs key)
|
||||
@@ -229,7 +233,16 @@
|
||||
$)))
|
||||
value1' (normalize-value value1)
|
||||
value2' (normalize-value value2)]
|
||||
(not= value1' value2'))))
|
||||
(cond
|
||||
(= value1' value2')
|
||||
false
|
||||
|
||||
:else
|
||||
(let [n1 (when (string? value1') (d/parse-double value1'))
|
||||
n2 (when (string? value2') (d/parse-double value2'))]
|
||||
(if (and (some? n1) (some? n2))
|
||||
(not (mth/close? n1 n2))
|
||||
true))))))
|
||||
|
||||
(defn- compare-text-content
|
||||
"Given two content text structures, conformed by maps and vectors,
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns common-tests.files.repair-test
|
||||
"Tests for the validate / repair functions in app.common.files.validate
|
||||
and app.common.files.repair.
|
||||
|
||||
The tests generate cases of broken files and check that the validation functions
|
||||
generate accurate errors, and that the repair functions return the file to
|
||||
a stable state."
|
||||
(:require
|
||||
[app.common.files.repair :as cfr]
|
||||
[app.common.files.validate :as cfv]
|
||||
[app.common.test-helpers.components :as thc]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.test-helpers.variants :as thv]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
(t/deftest repair-main-instance-not-a-variant
|
||||
(t/testing "detect and repair a variant component whose root shape is not a variant"
|
||||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||||
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
|
||||
(ths/update-shape :root1 :variant-id nil))
|
||||
|
||||
errors (cfv/validate-file file {})
|
||||
changes (cfr/repair-file file {} errors)
|
||||
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
|
||||
errors' (cfv/validate-file file' {})
|
||||
|
||||
root1' (ths/get-shape file' :root1 :page-label :page1)]
|
||||
|
||||
(t/is (= 2 (count errors))) ;; There are two different checks that detect the same problem
|
||||
(t/is (= :main-instance-not-a-variant (:code (first errors))))
|
||||
|
||||
(t/is (nil? errors'))
|
||||
(t/is (= (thi/id :variant1) (:variant-id root1'))))))
|
||||
|
||||
(t/deftest repair-invalid-variant-id-variant-component-bad-id
|
||||
(t/testing "detect and repair a variant component whose variant id does not match the container's id"
|
||||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||||
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
|
||||
(ths/update-shape :root1 :variant-id (uuid/next)))
|
||||
|
||||
errors (cfv/validate-file file {})
|
||||
changes (cfr/repair-file file {} errors)
|
||||
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
|
||||
errors' (cfv/validate-file file' {})
|
||||
|
||||
root1' (ths/get-shape file' :root1 :page-label :page1)]
|
||||
|
||||
(t/is (= 2 (count errors))) ;; There are two different validation that actually check the same problem
|
||||
(t/is (= :main-instance-invalid-variant-id (:code (first errors))))
|
||||
(t/is (= :variant-component-bad-id (:code (second errors))))
|
||||
|
||||
(t/is (nil? errors'))
|
||||
(t/is (= (thi/id :variant1) (:variant-id root1'))))))
|
||||
|
||||
(t/deftest repair-invalid-variant-properties
|
||||
(t/testing "detect and repair a second variant component whose properties do not match the first variant component's properties"
|
||||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||||
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
|
||||
;; Component1 has ["Property 1", "Property 2"], component2 gets ["Property 1", "Property 3"]
|
||||
;; This breaks validation: prop-names mismatch (missing "Property 2", extra "Property 3")
|
||||
(thc/update-component :component1 {:variant-properties [{:name "Property 1" :value "Value1"}
|
||||
{:name "Property 2" :value "ValueA"}]})
|
||||
(thc/update-component :component2 {:variant-properties [{:name "Property 1" :value "Value2"}
|
||||
{:name "Property 3" :value "ValueB"}]})
|
||||
(ths/update-shape :root1 :variant-name "Value1, ValueA")
|
||||
(ths/update-shape :root2 :variant-name "Value2, ValueB"))
|
||||
|
||||
errors (cfv/validate-file file {})
|
||||
changes (cfr/repair-file file {} errors)
|
||||
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
|
||||
errors' (cfv/validate-file file' {})
|
||||
|
||||
comp1' (thc/get-component file' :component1)
|
||||
comp2' (thc/get-component file' :component2)
|
||||
root1' (ths/get-shape file' :root1)
|
||||
root2' (ths/get-shape file' :root2)]
|
||||
|
||||
(t/is (= 1 (count errors)))
|
||||
(t/is (= :invalid-variant-properties (:code (first errors))))
|
||||
|
||||
(t/is (nil? errors'))
|
||||
|
||||
;; After repair, component1's properties are rebuilt to match component2's property names
|
||||
;; (the first child in the variant container is root2, so prop-names come from component2)
|
||||
;; "Property 1" keeps its value, "Property 3" is added with empty value, "Property 2" is removed
|
||||
(t/is (= [{:name "Property 1" :value "Value1"}
|
||||
{:name "Property 3" :value ""}]
|
||||
(:variant-properties comp1')))
|
||||
|
||||
(t/is (= "Value1" (:variant-name root1')))
|
||||
|
||||
;; Component2 is unchanged (it was the reference for the property names)
|
||||
(t/is (= [{:name "Property 1" :value "Value2"}
|
||||
{:name "Property 3" :value "ValueB"}]
|
||||
(:variant-properties comp2')))
|
||||
|
||||
(t/is (= "Value2, ValueB" (:variant-name root2'))))))
|
||||
|
||||
(t/deftest repair-variant-not-main
|
||||
(t/testing "detect and repair a non-main-instance shape inside a variant container"
|
||||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||||
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
|
||||
;; Add a third child to the variant container with :variant-id but NOT a main-instance
|
||||
(ths/add-sample-shape :bad-shape
|
||||
:type :frame
|
||||
:parent-label :variant1
|
||||
:variant-id (thi/id :variant1)
|
||||
:variant-name "")
|
||||
;; Add a child to the bad shape (to verify the repair deletes it too)
|
||||
(ths/add-sample-shape :bad-child
|
||||
:type :rect
|
||||
:parent-label :bad-shape))
|
||||
|
||||
errors (cfv/validate-file file {})
|
||||
changes (cfr/repair-file file {} errors)
|
||||
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
|
||||
errors' (cfv/validate-file file' {})
|
||||
|
||||
bad-shape' (ths/get-shape file' :bad-shape)
|
||||
bad-child' (ths/get-shape file' :bad-child)]
|
||||
|
||||
(t/is (= 4 (count errors))) ;; The bad container also triggers other errors
|
||||
(t/is (= :invalid-variant-properties (:code (nth errors 0))))
|
||||
(t/is (= :variant-not-main (:code (nth errors 1))))
|
||||
(t/is (= :variant-component-bad-name (:code (nth errors 2))))
|
||||
(t/is (= :variant-component-bad-id (:code (nth errors 3))))
|
||||
(t/is (nil? errors'))
|
||||
|
||||
(t/is (nil? bad-shape'))
|
||||
(t/is (nil? bad-child')))))
|
||||
|
||||
(t/deftest repair-parent-not-variant
|
||||
(t/testing "detect and repair a variant shape whose parent is not a variant-container"
|
||||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||||
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
|
||||
;; Break the variant container
|
||||
(ths/update-shape :variant1 :is-variant-container false))
|
||||
|
||||
errors (cfv/validate-file file {})
|
||||
changes (cfr/repair-file file {} errors)
|
||||
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
|
||||
errors' (cfv/validate-file file' {})
|
||||
|
||||
container' (ths/get-shape file' :variant1)]
|
||||
|
||||
(t/is (= 2 (count errors))) ;; The error is detected twice, once for each child of the variant container
|
||||
(t/is (= :parent-not-variant (:code (first errors))))
|
||||
(t/is (= :parent-not-variant (:code (second errors))))
|
||||
(t/is (nil? errors'))
|
||||
|
||||
(t/is (true? (:is-variant-container container'))))))
|
||||
|
||||
(t/deftest repair-variant-main-bad-name
|
||||
(t/testing "detect and repair a main instance whose name doesn't match the variant container's name"
|
||||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||||
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
|
||||
;; Change root1's name so it doesn't match the container
|
||||
(ths/update-shape :root1 :name "WrongName"))
|
||||
|
||||
errors (cfv/validate-file file {})
|
||||
changes (cfr/repair-file file {} errors)
|
||||
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
|
||||
errors' (cfv/validate-file file' {})
|
||||
root1' (ths/get-shape file' :root1)]
|
||||
|
||||
(t/is (= 1 (count errors)))
|
||||
(t/is (= :variant-main-bad-name (:code (first errors))))
|
||||
(t/is (nil? errors'))
|
||||
(t/is (= "Board" (:name root1'))))))
|
||||
|
||||
(t/deftest repair-variant-main-bad-variant-name
|
||||
(t/testing "detect and repair a variant shape whose :variant-name doesn't match the component's properties"
|
||||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||||
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
|
||||
(thc/update-component :component1 {:variant-properties [{:name "Property 1" :value "Value1"}
|
||||
{:name "Property 2" :value "ValueA"}]})
|
||||
(thc/update-component :component2 {:variant-properties [{:name "Property 1" :value "Value2"}
|
||||
{:name "Property 2" :value "ValueB"}]})
|
||||
;; Change root1's :variant-name to something wrong
|
||||
(ths/update-shape :root1 :variant-name "WrongVariantName")
|
||||
(ths/update-shape :root2 :variant-name "Value2, ValueB"))
|
||||
|
||||
errors (cfv/validate-file file {})
|
||||
changes (cfr/repair-file file {} errors)
|
||||
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
|
||||
errors' (cfv/validate-file file' {})
|
||||
|
||||
root1' (ths/get-shape file' :root1)]
|
||||
|
||||
(t/is (= 1 (count errors)))
|
||||
(t/is (= :variant-main-bad-variant-name (:code (first errors))))
|
||||
(t/is (nil? errors'))
|
||||
(t/is (= "Value1, ValueA" (:variant-name root1'))))))
|
||||
|
||||
(t/deftest repair-variant-component-bad-name
|
||||
(t/testing "detect and repair a variant component whose path/name doesn't match the container name"
|
||||
(let [file (-> (thf/sample-file :file1 :page-label :page1)
|
||||
(thv/add-variant :variant1 :component1 :root1 :component2 :root2)
|
||||
;; Update names to have path structure
|
||||
(ths/update-shape :variant1 :name "Group / Subgroup / Component")
|
||||
(ths/update-shape :root1 :name "Group / Subgroup / Component")
|
||||
(ths/update-shape :root2 :name "Group / Subgroup / Component")
|
||||
;; Update component paths and names
|
||||
(thc/update-component :component1 {:path "Group / Subgroup" :name "Component"})
|
||||
(thc/update-component :component2 {:path "Group / Subgroup" :name "Component"})
|
||||
;; Break component1's name
|
||||
(thc/update-component :component1 {:name "WrongName"}))
|
||||
|
||||
errors (cfv/validate-file file {})
|
||||
changes (cfr/repair-file file {} errors)
|
||||
file' (thf/apply-changes file {:redo-changes changes} :validate? false)
|
||||
errors' (cfv/validate-file file' {})
|
||||
comp1' (thc/get-component file' :component1)]
|
||||
|
||||
(t/is (= 1 (count errors)))
|
||||
(t/is (= :variant-component-bad-name (:code (first errors))))
|
||||
(t/is (nil? errors'))
|
||||
(t/is (= "Group / Subgroup" (:path comp1')))
|
||||
(t/is (= "Component" (:name comp1'))))))
|
||||
@@ -78,6 +78,14 @@
|
||||
(def content-changed-line-height
|
||||
(assoc-in content-base [:children 0 :children 0 :line-height] "1.5"))
|
||||
|
||||
;; Token/WASM may store full float precision; editor round-trips often
|
||||
;; truncate (e.g. CSS / f32). These must compare as equal.
|
||||
(def content-line-height-full-precision
|
||||
(assoc-in content-base [:children 0 :children 0 :line-height] "1.3333333333333333"))
|
||||
|
||||
(def content-line-height-truncated
|
||||
(assoc-in content-base [:children 0 :children 0 :line-height] "1.33333"))
|
||||
|
||||
(def content-redundant-span-line-height
|
||||
(assoc-in content-base [:children 0 :children 0 :children 0 :line-height] "1.5"))
|
||||
|
||||
@@ -208,6 +216,8 @@
|
||||
;; Other text-node-attr categories
|
||||
attrs-font-family (cttx/get-diff-attrs content-base content-changed-font-family)
|
||||
attrs-line-height (cttx/get-diff-attrs content-base content-changed-line-height)
|
||||
attrs-line-height-precision (cttx/get-diff-attrs content-line-height-full-precision
|
||||
content-line-height-truncated)
|
||||
attrs-span-line-height (cttx/get-diff-attrs content-base content-redundant-span-line-height)
|
||||
attrs-roundtrip-line-height (cttx/get-diff-attrs content-token-like-line-height
|
||||
content-after-editor-roundtrip)
|
||||
@@ -242,6 +252,7 @@
|
||||
;; Each text-node-attr category reports correct attr key
|
||||
(t/is (= #{:font-family} attrs-font-family))
|
||||
(t/is (= #{:line-height} attrs-line-height))
|
||||
(t/is (= #{} attrs-line-height-precision))
|
||||
(t/is (= #{} attrs-span-line-height))
|
||||
(t/is (= #{} attrs-roundtrip-line-height))
|
||||
(t/is (= #{} attrs-nil-typography-refs))
|
||||
|
||||
@@ -588,7 +588,7 @@ PENPOT_FLAGS: [...] enable-auto-file-snapshot # Enable automatic v
|
||||
|
||||
# Backend
|
||||
PENPOT_AUTO_FILE_SNAPSHOT_EVERY: 5 # How many save operations trigger the auto-save-version?
|
||||
PENPOT_AUTO_FILE_SNAPSHOT_TIIMEOUT: "1h" # How often is an automatic save forced even if the `every` trigger is not met?
|
||||
PENPOT_AUTO_FILE_SNAPSHOT_TIMEOUT: "1h" # How often is an automatic save forced even if the `every` trigger is not met?
|
||||
```
|
||||
|
||||
Setting custom values for auto-file-snapshot does not change the behaviour for manual versions.
|
||||
|
||||
@@ -34,8 +34,11 @@
|
||||
"watch": "pnpm run watch:app",
|
||||
"build:app": "clojure -M:dev:shadow-cljs release main",
|
||||
"build": "pnpm run clear:shadow-cache && pnpm run build:app",
|
||||
"fmt": "cljfmt fix --parallel=true src/",
|
||||
"check-fmt": "cljfmt check --parallel=true src/",
|
||||
"lint": "clj-kondo --parallel --lint src/"
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"check-fmt:clj": "cljfmt check --parallel=true src/ test/",
|
||||
"lint:clj": "clj-kondo --parallel --lint src/ test/",
|
||||
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
||||
"test": "pnpm run build:test && node target/tests/test.js",
|
||||
"test:quiet": "node ./scripts/test-quiet.js"
|
||||
}
|
||||
}
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run test;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const BUILD_STEPS = [
|
||||
{ label: "Building test bundle", cmd: "pnpm", args: ["run", "build:test"] },
|
||||
];
|
||||
|
||||
const progress = (msg) => process.stderr.write(`${msg}\n`);
|
||||
|
||||
for (const step of BUILD_STEPS) {
|
||||
progress(`${step.label}...`);
|
||||
const result = spawnSync(step.cmd, step.args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
progress(`${step.label} failed`);
|
||||
if (result.stdout?.length) process.stdout.write(result.stdout);
|
||||
if (result.stderr?.length) process.stderr.write(result.stderr);
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
progress("Running tests...");
|
||||
const result = spawnSync(
|
||||
"node",
|
||||
["target/tests/test.js", ...process.argv.slice(2)],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
process.exit(result.status ?? 1);
|
||||
@@ -31,4 +31,12 @@
|
||||
:pseudo-names true
|
||||
:pretty-print true
|
||||
:anon-fn-naming-policy :off
|
||||
:source-map-detail-level :all}}}}}
|
||||
:source-map-detail-level :all}}}
|
||||
|
||||
:test
|
||||
{:target :esm
|
||||
:output-dir "target/tests"
|
||||
:runtime :node
|
||||
:js-options {:js-provider :import}
|
||||
:modules
|
||||
{:test {:init-fn exporter-tests.runner/-main}}}}}
|
||||
@@ -117,7 +117,7 @@
|
||||
[file-id paths]
|
||||
(p/let [prefix (str/concat "penpot.pdfunite." file-id ".")
|
||||
path (sh/tempfile :prefix prefix :suffix ".pdf")]
|
||||
(sh/run-cmd! (str "pdfunite " (str/join " " paths) " " path))
|
||||
(apply sh/run-cmd! "pdfunite" (conj (vec paths) path))
|
||||
path))
|
||||
|
||||
(defn- move-file
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
:webp (p/let [png-path (sh/tempfile :prefix "penpot.tmp.bitmap." :suffix ".png")]
|
||||
;; playwright only supports jpg and png, we need to convert it afterwards
|
||||
(bw/screenshot node {:omit-background? true :type :png :path png-path})
|
||||
(sh/run-cmd! (str "convert " png-path " -quality 100 WEBP:" path))))
|
||||
(sh/run-cmd! "convert" png-path "-quality" "100" (str "WEBP:" path))))
|
||||
(on-object (assoc object :path path))))
|
||||
|
||||
(render [uri page]
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
["xml-js" :as xml]
|
||||
[app.browser :as bw]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.renderer.svg-gradient :as svg-gradient]
|
||||
[app.util.mime :as mime]
|
||||
[app.util.shell :as sh]
|
||||
[clojure.walk :as walk]
|
||||
@@ -125,19 +128,23 @@
|
||||
(letfn [(convert-to-ppm [pngpath]
|
||||
(let [ppmpath (str/concat pngpath "origin.ppm")]
|
||||
(l/trace :fn :convert-to-ppm :path ppmpath)
|
||||
(-> (sh/run-cmd! (str "convert " pngpath " " ppmpath))
|
||||
(-> (sh/run-cmd! "convert" pngpath ppmpath)
|
||||
(p/then (constantly ppmpath)))))
|
||||
|
||||
(trace-color-mask [pbmpath]
|
||||
(l/trace :fn :trace-color-mask :pbmpath pbmpath)
|
||||
(let [svgpath (str/concat pbmpath ".svg")]
|
||||
(-> (sh/run-cmd! (str "potrace --flat -b svg " pbmpath " -o " svgpath))
|
||||
(-> (sh/run-cmd! "potrace" "--flat" "-b" "svg" pbmpath "-o" svgpath)
|
||||
(p/then (constantly svgpath)))))
|
||||
|
||||
(generate-color-layer [ppmpath color]
|
||||
(when-not (ctc/hex-color-string? color)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-color
|
||||
:hint (str "invalid hex color: " color)))
|
||||
(l/trace :fn :generate-color-layer :ppmpath ppmpath :color color)
|
||||
(let [pbmpath (str/concat ppmpath ".mask-" (subs color 1) ".pbm")]
|
||||
(-> (sh/run-cmd! (str/format "ppmcolormask \"%s\" %s" color ppmpath))
|
||||
(-> (sh/run-cmd! "ppmcolormask" color ppmpath)
|
||||
(p/then (fn [stdout]
|
||||
(-> (sh/write-file! pbmpath stdout)
|
||||
(p/then (constantly pbmpath)))))
|
||||
@@ -166,33 +173,11 @@
|
||||
:else
|
||||
(update node "attributes" assoc "fill" color))))
|
||||
|
||||
(get-stops [data]
|
||||
(->> (get-in data ["gradient" "stops"])
|
||||
(mapv (fn [stop-data]
|
||||
{"type" "element"
|
||||
"name" "stop"
|
||||
"attributes" {"offset" (get stop-data "offset")
|
||||
"stop-color" (get stop-data "color")
|
||||
"stop-opacity" (get stop-data "opacity")}}))))
|
||||
|
||||
(data->gradient-def [id [color data]]
|
||||
(let [id (str "gradient-" id "-" (subs color 1))]
|
||||
(if (= type "linear")
|
||||
{"type" "element"
|
||||
"name" "linearGradient"
|
||||
"attributes" {"id" id "x1" "0.5" "y1" "1" "x2" "0.5" "y2" "0"}
|
||||
"elements" (get-stops data)}
|
||||
|
||||
{"type" "element"
|
||||
"name" "radialGradient"
|
||||
"attributes" {"id" id "cx" "0.5" "cy" "0.5" "r" "0.5"}
|
||||
"elements" (get-stops data)})))
|
||||
|
||||
(get-gradients [id mapping]
|
||||
(->> mapping
|
||||
(filter (fn [[_color data]]
|
||||
(= (get data "type") "gradient")))
|
||||
(mapv (partial data->gradient-def id))))
|
||||
(mapv (partial svg-gradient/data->gradient-def id))))
|
||||
|
||||
(join-color-layers [{:keys [id x y width height mapping] :as node} layers]
|
||||
(l/trace :fn :join-color-layers :mapping mapping)
|
||||
@@ -369,4 +354,3 @@
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(bw/exec! (prepare-options uri)
|
||||
(partial render uri)))))
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.renderer.svg-gradient)
|
||||
|
||||
(defn- get-stops
|
||||
[data]
|
||||
(->> (get-in data ["gradient" "stops"])
|
||||
(mapv (fn [stop-data]
|
||||
{"type" "element"
|
||||
"name" "stop"
|
||||
"attributes" {"offset" (get stop-data "offset")
|
||||
"stop-color" (get stop-data "color")
|
||||
"stop-opacity" (get stop-data "opacity")}}))))
|
||||
|
||||
(defn data->gradient-def
|
||||
[id [color data]]
|
||||
(let [id (str "gradient-" id "-" (subs color 1))
|
||||
gradient-type (get-in data ["gradient" "type"])]
|
||||
(if (= gradient-type "linear")
|
||||
{"type" "element"
|
||||
"name" "linearGradient"
|
||||
"attributes" {"id" id "x1" "0.5" "y1" "1" "x2" "0.5" "y2" "0"}
|
||||
"elements" (get-stops data)}
|
||||
|
||||
{"type" "element"
|
||||
"name" "radialGradient"
|
||||
"attributes" {"id" id "cx" "0.5" "cy" "0.5" "r" "0.5"}
|
||||
"elements" (get-stops data)})))
|
||||
@@ -94,14 +94,14 @@
|
||||
(.readFile fs/promises fpath))
|
||||
|
||||
(defn run-cmd!
|
||||
[cmd]
|
||||
[cmd & args]
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(l/trace :fn :run-cmd :cmd cmd)
|
||||
(proc/exec cmd #js {:encoding "buffer"}
|
||||
(fn [error stdout _stderr]
|
||||
;; (l/trace :fn :run-cmd :stdout stdout)
|
||||
(if error
|
||||
(reject error)
|
||||
(resolve stdout)))))))
|
||||
(l/trace :fn :run-cmd :cmd cmd :args args)
|
||||
(proc/execFile cmd (clj->js args) #js {:encoding "buffer"}
|
||||
(fn [error stdout _stderr]
|
||||
;; (l/trace :fn :run-cmd :stdout stdout)
|
||||
(if error
|
||||
(reject error)
|
||||
(resolve stdout)))))))
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns exporter-tests.renderer-svg-test
|
||||
(:require
|
||||
[app.renderer.svg-gradient :as svg-gradient]
|
||||
[cljs.test :refer [deftest is testing]]))
|
||||
|
||||
(def gradient-stops
|
||||
[{"color" "#000000" "offset" 0 "opacity" 1}
|
||||
{"color" "#ffffff" "offset" 1 "opacity" 1}])
|
||||
|
||||
(deftest creates-the-correct-gradient-element
|
||||
(doseq [[gradient-type element-name]
|
||||
[["linear" "linearGradient"]
|
||||
["radial" "radialGradient"]]]
|
||||
(testing gradient-type
|
||||
(let [gradient-data {"type" "gradient"
|
||||
"gradient" {"type" gradient-type
|
||||
"stops" gradient-stops}}
|
||||
result (svg-gradient/data->gradient-def "text-id" ["#000001" gradient-data])]
|
||||
(is (= element-name (get result "name")))))))
|
||||
@@ -0,0 +1,172 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns exporter-tests.runner
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[cljs.test :as t]
|
||||
[clojure.string :as str]
|
||||
[clojure.tools.cli :refer [parse-opts]]
|
||||
[exporter-tests.renderer-svg-test]
|
||||
[exporter-tests.shell-test]
|
||||
[goog.object :as gobj]))
|
||||
|
||||
(enable-console-print!)
|
||||
|
||||
(def test-namespaces
|
||||
['exporter-tests.renderer-svg-test
|
||||
'exporter-tests.shell-test])
|
||||
|
||||
(assert (every? find-ns-obj test-namespaces)
|
||||
"test-namespaces contains a namespace that isn't required in runner.cljs")
|
||||
|
||||
(defmethod t/report [:cljs.test/default :begin-test-var]
|
||||
[m]
|
||||
(let [v (:var m)]
|
||||
(println (str " ▸ " (:ns (meta v)) "/" (:name (meta v))))))
|
||||
|
||||
(defmethod t/report [:cljs.test/default :end-run-tests]
|
||||
[result]
|
||||
(.exit js/process (if (cljs.test/successful? result) 0 1)))
|
||||
|
||||
(def ^:private log-levels
|
||||
#{:trace :debug :info :warn :error})
|
||||
|
||||
(def cli-options
|
||||
[["-f" "--focus FOCUS" "Run one test namespace or one test var, e.g. exporter-tests.renderer-svg-test/creates-the-correct-gradient-element"]
|
||||
["-l" "--log-level LEVEL" "Set app logger level: trace|debug|info|warn|error"
|
||||
:parse-fn keyword
|
||||
:validate [log-levels "must be one of trace, debug, info, warn, error"]]
|
||||
["-h" "--help"]])
|
||||
|
||||
(defn- argv
|
||||
[]
|
||||
(let [args (->> (.-argv js/process)
|
||||
(array-seq)
|
||||
(drop 2))]
|
||||
;; `pnpm run test -- --focus ...` forwards the separator to the node
|
||||
;; process, so drop one leading `--` before handing args to tools.cli.
|
||||
(cond-> args
|
||||
(= "--" (first args)) rest)))
|
||||
|
||||
(defn- usage
|
||||
[summary]
|
||||
(str "Usage: node target/tests/test.js [options]\n\n"
|
||||
"Options:\n"
|
||||
summary "\n\n"
|
||||
"Build first with: pnpm run build:test\n\n"
|
||||
"Focus examples:\n"
|
||||
" node target/tests/test.js --focus exporter-tests.renderer-svg-test\n"
|
||||
" node target/tests/test.js --focus exporter-tests.renderer-svg-test/creates-the-correct-gradient-element\n\n"
|
||||
"Log level example:\n"
|
||||
" node target/tests/test.js --focus exporter-tests.renderer-svg-test --log-level warn"))
|
||||
|
||||
(defn- fail!
|
||||
[message]
|
||||
(js/console.error message)
|
||||
(.exit js/process 1))
|
||||
|
||||
(defn- parse-focus
|
||||
[focus]
|
||||
(let [[ns-name test-name & extra] (str/split focus #"/")]
|
||||
(cond
|
||||
(or (str/blank? ns-name) (seq extra))
|
||||
(fail! (str "Invalid --focus value: " focus))
|
||||
|
||||
(some? test-name)
|
||||
{:ns (symbol ns-name) :test test-name}
|
||||
|
||||
:else
|
||||
{:ns (symbol ns-name)})))
|
||||
|
||||
(defn- fixture-value
|
||||
[ns-obj fixture-name]
|
||||
(let [value (gobj/get ns-obj (munge fixture-name))]
|
||||
(when-not (undefined? value)
|
||||
value)))
|
||||
|
||||
(defn- ns-test-vars
|
||||
[ns-sym]
|
||||
(when-let [ns-obj (find-ns-obj ns-sym)]
|
||||
(->> (js-keys ns-obj)
|
||||
(keep (fn [key]
|
||||
(some-> (gobj/get ns-obj key)
|
||||
(.-cljs$lang$var))))
|
||||
(filter (comp :test meta))
|
||||
(sort-by (comp :line meta)))))
|
||||
|
||||
(defn- ns-fixtures
|
||||
[ns-sym vars]
|
||||
(when-let [ns-obj (find-ns-obj ns-sym)]
|
||||
(let [ns-key (or (some-> vars first meta :ns) ns-sym)
|
||||
once-fixtures (fixture-value ns-obj "cljs-test-once-fixtures")
|
||||
each-fixtures (fixture-value ns-obj "cljs-test-each-fixtures")]
|
||||
{:once (when once-fixtures {ns-key once-fixtures})
|
||||
:each (when each-fixtures {ns-key each-fixtures})})))
|
||||
|
||||
(defn- selected-tests
|
||||
[{:keys [ns test]}]
|
||||
(when-not (some #{ns} test-namespaces)
|
||||
(fail! (str "Unknown test namespace: " ns)))
|
||||
(let [vars (vec (ns-test-vars ns))]
|
||||
(when (empty? vars)
|
||||
(fail! (str "No tests found in namespace: " ns)))
|
||||
(if test
|
||||
(let [test-sym (symbol test)
|
||||
test-var (some #(when (= test-sym (:name (meta %))) %) vars)]
|
||||
(if test-var
|
||||
{:vars [test-var]
|
||||
:fixtures (ns-fixtures ns [test-var])}
|
||||
(fail! (str "Unknown test var: " ns "/" test))))
|
||||
{:vars vars
|
||||
:fixtures (ns-fixtures ns vars)})))
|
||||
|
||||
(defn- merge-fixtures
|
||||
[fixtures]
|
||||
{:once (apply merge (keep :once fixtures))
|
||||
:each (apply merge (keep :each fixtures))})
|
||||
|
||||
(defn- run-test-vars!
|
||||
[tests]
|
||||
(let [vars (vec (mapcat :vars tests))
|
||||
fixtures (merge-fixtures (map :fixtures tests))
|
||||
env (assoc (t/empty-env)
|
||||
:once-fixtures (:once fixtures)
|
||||
:each-fixtures (:each fixtures))
|
||||
summary (volatile! {:test 0 :pass 0 :fail 0 :error 0 :type :summary})]
|
||||
(t/set-env! env)
|
||||
(t/run-block
|
||||
(concat (t/test-vars-block vars)
|
||||
[(fn []
|
||||
(vswap! summary
|
||||
(partial merge-with +)
|
||||
(:report-counters (t/get-current-env))))
|
||||
(fn []
|
||||
(t/report @summary)
|
||||
(t/report (assoc @summary :type :end-run-tests)))]))))
|
||||
|
||||
(defn- run-focused-test!
|
||||
[focus]
|
||||
(run-test-vars! [(selected-tests (parse-focus focus))]))
|
||||
|
||||
(defn -main
|
||||
[]
|
||||
(let [{:keys [options errors summary]} (parse-opts (argv) cli-options)]
|
||||
(cond
|
||||
(seq errors)
|
||||
(fail! (str/join "\n" errors))
|
||||
|
||||
(:help options)
|
||||
(do
|
||||
(println (usage summary))
|
||||
(.exit js/process 0))
|
||||
|
||||
:else
|
||||
(do
|
||||
(l/setup! {:app (or (:log-level options) :warn)})
|
||||
(if (:focus options)
|
||||
(run-focused-test! (:focus options))
|
||||
(run-test-vars! (map #(selected-tests {:ns %}) test-namespaces)))))))
|
||||
@@ -0,0 +1,70 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns exporter-tests.shell-test
|
||||
"Tests to verify GHSA-4f36-m4hj-cv86 is fixed: OS Command Injection in SVG exporter.
|
||||
These tests prove that:
|
||||
1. execFile does NOT interpret shell metacharacters (safe execution)
|
||||
2. Malicious colors fail validation regex
|
||||
3. The injection does NOT execute commands (no RCE)"
|
||||
(:require
|
||||
["node:child_process" :as proc]
|
||||
["node:fs" :as fs]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(def ^:private hex-color-rx
|
||||
#"^#(?:[0-9a-fA-F]{3}){1,2}$")
|
||||
|
||||
(defn- valid-hex-color?
|
||||
[color]
|
||||
(and (string? color)
|
||||
(some? (re-matches hex-color-rx color))))
|
||||
|
||||
(t/deftest execfile-does-not-interpret-shell-metacharacters
|
||||
(t/testing "Proves execFile passes arguments literally (no shell interpretation)"
|
||||
(t/async done
|
||||
(let [cmd "echo"
|
||||
args #js ["$(echo PWNED)"]]
|
||||
(proc/execFile cmd args #js {:encoding "buffer"}
|
||||
(fn [error stdout _stderr]
|
||||
(if error
|
||||
(do
|
||||
(t/is false (str "unexpected error: " (.-message error)))
|
||||
(done))
|
||||
(let [output (.toString stdout "utf8")]
|
||||
(t/is (= "$(echo PWNED)\n" output)
|
||||
"execFile passes $(...) literally, no shell interpretation")
|
||||
(done)))))))))
|
||||
|
||||
(t/deftest malicious-color-fails-validation
|
||||
(t/testing "Proves malicious colors are rejected by validation"
|
||||
(let [malicious "#000000$(echo PWNED)"
|
||||
valid-color "#000000"
|
||||
short-valid "#abc"]
|
||||
(t/is (not (valid-hex-color? malicious))
|
||||
"malicious color with $(...) fails validation")
|
||||
(t/is (valid-hex-color? valid-color)
|
||||
"valid 6-digit hex color passes validation")
|
||||
(t/is (valid-hex-color? short-valid)
|
||||
"valid 3-digit hex color passes validation"))))
|
||||
|
||||
(t/deftest execfile-does-not-execute-injected-commands
|
||||
(t/testing "Proves execFile does NOT execute injected commands (no RCE)"
|
||||
(t/async done
|
||||
(let [marker "/tmp/penpot-exporter-rce-test"
|
||||
malicious (str "#000000$(touch " marker ")")
|
||||
cmd "echo"
|
||||
args #js [malicious]]
|
||||
(when (fs/existsSync marker)
|
||||
(fs/unlinkSync marker))
|
||||
(proc/execFile cmd args #js {:encoding "buffer"}
|
||||
(fn [_error _stdout _stderr]
|
||||
;; Command completes (or fails), but no injection occurs
|
||||
(t/is (not (fs/existsSync marker))
|
||||
"no RCE: marker file was NOT created")
|
||||
(when (fs/existsSync marker)
|
||||
(fs/unlinkSync marker))
|
||||
(done)))))))
|
||||
@@ -79,15 +79,21 @@
|
||||
{:type :gradient
|
||||
:gradient fill-color-gradient}
|
||||
|
||||
(and (string? fill-color) (some? fill-opacity) (not= fill-opacity 1))
|
||||
(and (string? fill-color)
|
||||
(cc/hex-color-string? fill-color)
|
||||
(some? fill-opacity)
|
||||
(not= fill-opacity 1))
|
||||
{:type :transparent
|
||||
:hex fill-color
|
||||
:opacity fill-opacity}
|
||||
|
||||
(string? fill-color)
|
||||
(and (string? fill-color)
|
||||
(cc/hex-color-string? fill-color))
|
||||
{:type :solid
|
||||
:hex fill-color
|
||||
:map-to fill-color}))
|
||||
:map-to fill-color}
|
||||
|
||||
:else nil))
|
||||
|
||||
(defn- retrieve-colors
|
||||
"Given a text shape returns a triple with the values:
|
||||
|
||||
+4
-4
@@ -26,7 +26,7 @@ declare -A LINT_CMD=(
|
||||
[backend]="pnpm run lint"
|
||||
[common]="pnpm run lint:clj"
|
||||
[render-wasm]="./lint"
|
||||
[exporter]="pnpm run lint"
|
||||
[exporter]="pnpm run lint:clj"
|
||||
[mcp]=""
|
||||
[plugins]="pnpm run lint"
|
||||
[library]="pnpm run lint"
|
||||
@@ -37,7 +37,7 @@ declare -A TEST_CMD=(
|
||||
[backend]="clojure -M:dev:test"
|
||||
[common]="clojure -M:dev:test && pnpm run test:quiet"
|
||||
[render-wasm]="./test"
|
||||
[exporter]=""
|
||||
[exporter]="pnpm run test:quiet"
|
||||
[mcp]="pnpm run test"
|
||||
[plugins]="pnpm run test"
|
||||
[library]="pnpm run test"
|
||||
@@ -48,7 +48,7 @@ declare -A FMT_CHECK_CMD=(
|
||||
[backend]="pnpm run check-fmt"
|
||||
[common]="pnpm run check-fmt:clj && pnpm run check-fmt:js"
|
||||
[render-wasm]="cargo fmt --check"
|
||||
[exporter]="pnpm run check-fmt"
|
||||
[exporter]="pnpm run check-fmt:clj"
|
||||
[mcp]="pnpm run fmt:check"
|
||||
[plugins]="pnpm run format:check"
|
||||
[library]="pnpm run check-fmt"
|
||||
@@ -59,7 +59,7 @@ declare -A FMT_FIX_CMD=(
|
||||
[backend]="pnpm run fmt"
|
||||
[common]="pnpm run fmt:clj && pnpm run fmt:js"
|
||||
[render-wasm]="cargo fmt"
|
||||
[exporter]="pnpm run fmt"
|
||||
[exporter]="pnpm run fmt:clj"
|
||||
[mcp]="pnpm run fmt"
|
||||
[plugins]="pnpm run format"
|
||||
[library]="pnpm run fmt"
|
||||
|
||||
Reference in new issue
Block a user