mirror of
https://github.com/penpot/penpot.git
synced 2026-07-20 21:17:44 +00:00
Merge remote-tracking branch 'origin/staging'
This commit is contained in:
commit
3add7211a5
@ -6,26 +6,20 @@ description: Evaluate Clojure code via nREPL using the standalone tools/nrepl-ev
|
||||
# nREPL Eval
|
||||
|
||||
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
|
||||
`tools/nrepl-eval.mjs` — a standalone CLI application.
|
||||
`tools/nrepl-eval.mjs`.
|
||||
|
||||
Session state (defs, in-ns, etc.) persists across invocations via a stored
|
||||
session ID, so you can build up state incrementally.
|
||||
Full documentation: `mem:tools/nrepl-eval` (file: `.serena/memories/tools/nrepl-eval.md`)
|
||||
|
||||
## Usage
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
node tools/nrepl-eval.mjs [options] [<code>]
|
||||
```
|
||||
|
||||
The tool is also executable directly:
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs [options] [<code>]
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--backend` | Connect to backend nREPL (port 6064) | — |
|
||||
| `--frontend` | Connect to frontend nREPL (port 3447) | — |
|
||||
| `-p, --port PORT` | nREPL server port | `6064` |
|
||||
| `-H, --host HOST` | nREPL server host | `127.0.0.1` |
|
||||
| `-t, --timeout MS` | Timeout in milliseconds | `120000` |
|
||||
@ -33,88 +27,11 @@ The tool is also executable directly:
|
||||
| `-e, --last-error` | Evaluate `*e` to retrieve the last exception | — |
|
||||
| `-h, --help` | Show help message | — |
|
||||
|
||||
## When to Use
|
||||
## Examples
|
||||
|
||||
Use this tool when you need to:
|
||||
|
||||
1. **Evaluate Clojure code** during development — test functions, inspect
|
||||
state, or run experiments against a running Clojure process.
|
||||
2. **Verify that edited files compile** — require namespaces with `:reload`
|
||||
to pick up changes.
|
||||
3. **Inspect the last exception** after a failed evaluation — use `-e` to
|
||||
print the error stored in `*e`.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Session management
|
||||
|
||||
Sessions are persisted to `/tmp/penpot-nrepl-session-<host>-<port>`. State
|
||||
carries across calls automatically:
|
||||
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs '(def x 42)'
|
||||
./tools/nrepl-eval.mjs 'x'
|
||||
# => 42
|
||||
```
|
||||
|
||||
Reset the session to start fresh:
|
||||
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs --reset-session '(def x 0)'
|
||||
```
|
||||
|
||||
### 2. Evaluate code
|
||||
|
||||
**Single expression (inline) — uses default port 6064:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs '(+ 1 2 3)'
|
||||
```
|
||||
|
||||
**Multiple expressions via heredoc (recommended — avoids escaping issues):**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs <<'EOF'
|
||||
(def x 10)
|
||||
(+ x 20)
|
||||
EOF
|
||||
```
|
||||
|
||||
**Override with a different port:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs -p 7888 '(+ 1 2 3)'
|
||||
```
|
||||
|
||||
### 3. Inspect last exception
|
||||
|
||||
After code throws an error, retrieve the full exception details:
|
||||
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs --backend '(+ 1 2 3)'
|
||||
./tools/nrepl-eval.mjs --frontend '(js/alert "hi")'
|
||||
./tools/nrepl-eval.mjs -e
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Require a namespace with reload:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)"
|
||||
```
|
||||
|
||||
**Test a function:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs "(ns/my-function arg1 arg2)"
|
||||
```
|
||||
|
||||
**Long-running operation with custom timeout:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs -t 300000 "(long-running-fn)"
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Default port is 6064** — just pass code directly, no `-p` needed when
|
||||
your nREPL server is on 6064. Use `-p <PORT>` for a different port.
|
||||
- **Always use `:reload`** when requiring namespaces to pick up file changes.
|
||||
- **Session is reused** across invocations — defs, in-ns, and var bindings
|
||||
persist. Use `--reset-session` to clear.
|
||||
- **Do not start any server** — the tool connects to an existing nREPL
|
||||
server, it is not the agent's responsibility to start the nREPL server
|
||||
(assume the server is already running on the specified port).
|
||||
|
||||
@ -106,3 +106,4 @@ IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
|
||||
* **Coverage:** If code is added or modified in `src/`, corresponding tests in `test/backend_tests/` must be added or updated.
|
||||
* **Isolated run:** `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace.
|
||||
* **Regression run:** `clojure -M:dev:test` to ensure no regressions in related functional areas.
|
||||
* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:common/testing-principles`.
|
||||
|
||||
@ -49,6 +49,7 @@ Components, variants, and debugging:
|
||||
Text and tests:
|
||||
- Shared text data conversion, DraftJS compatibility, modern text content, and derived position data: `mem:common/text-subtleties`.
|
||||
- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/testing`.
|
||||
- Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:common/testing-principles`.
|
||||
|
||||
## Areas without focused memories
|
||||
|
||||
|
||||
47
.serena/memories/common/testing-principles.md
Normal file
47
.serena/memories/common/testing-principles.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Testing Principles (Cross-Cutting)
|
||||
|
||||
Shared testing principles for all modules (backend, frontend, common, render-wasm, plugins). Module-specific commands and helpers are in each module's own testing memory.
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Test State, Not Interactions** — assert on outcomes, not method calls; survives refactoring
|
||||
- **DAMP over DRY** — tests are specifications; duplication OK if each test is self-contained and readable
|
||||
- **Prefer Real Implementations** — hierarchy: Real > Fake > Stub > Mock; mock only at boundaries (network, filesystem, email)
|
||||
- **Arrange-Act-Assert** — every test: setup / action / verify
|
||||
- **One Assertion Per Concept** — each test verifies one behavior; split compound assertions
|
||||
- **Descriptive Test Names** — names read like specifications
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Testing implementation details → breaks on refactor
|
||||
- Flaky tests (timing, order-dependent) → erode trust
|
||||
- Mocking everything → tests pass, production breaks
|
||||
- No test isolation → pass individually, fail together
|
||||
- Testing framework code → waste of time
|
||||
- Snapshot abuse → nobody reviews, break on any change
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After completing any implementation:
|
||||
- Every new behavior has a corresponding test
|
||||
- All tests pass for touched modules
|
||||
- Bug fixes include a reproduction test that failed before the fix
|
||||
- Test names describe the behavior being verified
|
||||
- No tests were skipped or disabled
|
||||
|
||||
## Red Flags
|
||||
|
||||
- Writing code without corresponding tests
|
||||
- Tests that pass on the first run (may not be testing what you think)
|
||||
- Bug fixes without reproduction tests
|
||||
- Tests that test framework behavior instead of app behavior
|
||||
- Skipping tests to make the suite pass
|
||||
|
||||
## Rationalizations
|
||||
|
||||
- "I'll write tests after" → you won't; they'll test implementation not behavior
|
||||
- "Too simple to test" → simple gets complicated; test documents expected behavior
|
||||
- "Tests slow me down" → they speed up every future change
|
||||
- "I tested manually" → manual testing doesn't persist
|
||||
- "Code is self-explanatory" → tests ARE the specification
|
||||
- "Just a prototype" → prototypes become production; test debt accumulates
|
||||
@ -48,13 +48,24 @@ module. You can read it from `mem:<MODULE>/core`
|
||||
When working on devenv startup, compose layout, instance config (`defaults.env`),
|
||||
tmux session lifecycle, MinIO provisioning, or anything in `manage.sh`'s
|
||||
`*-devenv` commands, read `mem:devenv/core`.
|
||||
- `tools/` contains standalone dev utilities: `nrepl-eval.mjs` (backend REPL eval),
|
||||
`paren-repair.bb` (delimiter-error fixer, see `mem:tools/paren-repair`),
|
||||
`psql` / `db-schema` (PostgreSQL client and schema dump wrappers, see `mem:tools/psql`), and
|
||||
`taiga.py` / `gh.py` (issue management helpers).
|
||||
- `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it.
|
||||
- `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it.
|
||||
|
||||
# Dev tools
|
||||
|
||||
- `tools/nrepl-eval.mjs` — Evaluate Clojure/ClojureScript code via nREPL.
|
||||
Supports `--backend` (port 6064) and `--frontend` (port 3447) aliases.
|
||||
See `mem:tools/nrepl-eval`.
|
||||
- `tools/paren-repair.bb` — Fix mismatched delimiters in Clojure/CLJS files
|
||||
and reformat with cljfmt. Run before lint checks when LLM edits break parens.
|
||||
See `mem:tools/paren-repair`.
|
||||
- `tools/psql` — PostgreSQL client wrapper with devenv defaults.
|
||||
Companion: `tools/db-schema` for DDL dumps. See `mem:tools/psql`.
|
||||
- `tools/taiga.py` — Fetch public issues, user stories, and tasks from the
|
||||
Penpot Taiga project without authentication. See `mem:tools/taiga`.
|
||||
- `tools/gh.py` — GitHub operations helper: list milestone issues, fetch PR
|
||||
details, compare against CHANGES.md. Requires `gh` CLI. See `mem:tools/gh`.
|
||||
|
||||
# Dependency graph
|
||||
|
||||
`frontend -> common`, `backend -> common`, `exporter -> common`, and `frontend -> render-wasm`. Changes in `common` can
|
||||
|
||||
@ -52,6 +52,7 @@ Diagnostics and validation:
|
||||
- Source-edit compile/hot-reload diagnostics: `mem:frontend/compile-diagnostics`.
|
||||
- Runtime crash recovery: `mem:frontend/handling-crashes`.
|
||||
- Tests and live verification: `mem:frontend/testing`.
|
||||
- Cross-cutting testing principles and anti-patterns: `mem:common/testing-principles`.
|
||||
- Real pointer/keyboard gesture reproduction: `mem:frontend/playwright-gestures`.
|
||||
|
||||
## Areas without focused memories
|
||||
|
||||
80
.serena/memories/tools/gh.md
Normal file
80
.serena/memories/tools/gh.md
Normal file
@ -0,0 +1,80 @@
|
||||
# GitHub operations helper
|
||||
|
||||
`tools/gh.py` is a multi-purpose CLI for querying the penpot/penpot GitHub
|
||||
repository via GraphQL and REST APIs through the authenticated `gh` CLI.
|
||||
|
||||
## When to use
|
||||
|
||||
- Listing issues in a milestone (for changelog generation).
|
||||
- Finding issues with no milestone.
|
||||
- Fetching PR details by number or by milestone.
|
||||
- Comparing milestone issues against CHANGES.md to find missing entries.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `gh` CLI authenticated (`gh auth status`).
|
||||
- Python 3.8+.
|
||||
|
||||
## Subcommands
|
||||
|
||||
### `issues`
|
||||
|
||||
List issues in a milestone, with filtering by state, labels, and project status.
|
||||
|
||||
```bash
|
||||
# Closed issues in a milestone (default)
|
||||
python3 tools/gh.py issues "2.16.0"
|
||||
|
||||
# All issues in a milestone
|
||||
python3 tools/gh.py issues "2.16.0" --state all
|
||||
|
||||
# Issues with no milestone
|
||||
python3 tools/gh.py issues none
|
||||
python3 tools/gh.py issues none --state open
|
||||
|
||||
# Filter by label (include only)
|
||||
python3 tools/gh.py issues "2.16.0" --label "bug"
|
||||
python3 tools/gh.py issues "2.16.0" --label "bug,regression"
|
||||
|
||||
# Exclude by label
|
||||
python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
|
||||
|
||||
# Show only issues NOT yet in CHANGES.md
|
||||
python3 tools/gh.py issues "2.16.0" --compare CHANGES.md
|
||||
```
|
||||
|
||||
**Default filters** (override with flags):
|
||||
- Issues with type "Task" are excluded (`--include-tasks` to keep them).
|
||||
- Issues with "Rejected" project status are excluded (`--include-rejected` to keep them).
|
||||
|
||||
**Output**: JSON array to stdout; progress to stderr.
|
||||
|
||||
### `prs`
|
||||
|
||||
Fetch PR details by number or by milestone.
|
||||
|
||||
```bash
|
||||
# Fetch specific PRs
|
||||
python3 tools/gh.py prs 9179 9204 9311
|
||||
|
||||
# Read PR numbers from file
|
||||
python3 tools/gh.py prs --file prs.txt
|
||||
|
||||
# Read PR numbers from stdin
|
||||
cat prs.txt | python3 tools/gh.py prs --stdin
|
||||
|
||||
# All PRs in a milestone (default: merged only)
|
||||
python3 tools/gh.py prs --milestone "2.16.0"
|
||||
|
||||
# All PRs in a milestone (all states)
|
||||
python3 tools/gh.py prs --milestone "2.16.0" --state all
|
||||
```
|
||||
|
||||
**Output**: JSON array to stdout; progress to stderr.
|
||||
|
||||
## Key principles
|
||||
|
||||
- All output is JSON — pipe into `jq` or other tools for further processing.
|
||||
- Milestone lookup is by exact title match.
|
||||
- `issues` subcommand auto-paginates (100 items per page).
|
||||
- `prs` subcommand batches PR number lookups (50 per GraphQL query).
|
||||
126
.serena/memories/tools/nrepl-eval.md
Normal file
126
.serena/memories/tools/nrepl-eval.md
Normal file
@ -0,0 +1,126 @@
|
||||
# nREPL Eval
|
||||
|
||||
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
|
||||
`tools/nrepl-eval.mjs` — a standalone CLI application.
|
||||
|
||||
Session state (defs, in-ns, etc.) persists across invocations via a stored
|
||||
session ID, so you can build up state incrementally.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
node tools/nrepl-eval.mjs [options] [<code>]
|
||||
# or
|
||||
./tools/nrepl-eval.mjs [options] [<code>]
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--backend` | Connect to backend nREPL (port 6064) | — |
|
||||
| `--frontend` | Connect to frontend nREPL (port 3447) | — |
|
||||
| `-p, --port PORT` | nREPL server port | `6064` |
|
||||
| `-H, --host HOST` | nREPL server host | `127.0.0.1` |
|
||||
| `-t, --timeout MS` | Timeout in milliseconds | `120000` |
|
||||
| `--reset-session` | Discard stored session and start fresh | — |
|
||||
| `-e, --last-error` | Evaluate `*e` to retrieve the last exception | — |
|
||||
| `-h, --help` | Show help message | — |
|
||||
|
||||
- `--backend` and `--frontend` are mutually exclusive.
|
||||
- Explicit `--port` is overridden when `--backend`/`--frontend` is used.
|
||||
|
||||
## When to Use
|
||||
|
||||
1. **Evaluate Clojure code** during development — test functions, inspect
|
||||
state, or run experiments against a running Clojure process.
|
||||
2. **Verify that edited files compile** — require namespaces with `:reload`
|
||||
to pick up changes.
|
||||
3. **Inspect the last exception** after a failed evaluation — use `-e` to
|
||||
print the error stored in `*e`.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Session management
|
||||
|
||||
Sessions are persisted to `/tmp/penpot-nrepl-session-<host>-<port>`. State
|
||||
carries across calls automatically:
|
||||
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs '(def x 42)'
|
||||
./tools/nrepl-eval.mjs 'x'
|
||||
# => 42
|
||||
```
|
||||
|
||||
Reset the session to start fresh:
|
||||
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs --reset-session '(def x 0)'
|
||||
```
|
||||
|
||||
### Evaluate code
|
||||
|
||||
**Single expression (inline) — uses default port 6064:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs '(+ 1 2 3)'
|
||||
```
|
||||
|
||||
**Backend nREPL (explicit):**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs --backend '(+ 1 2 3)'
|
||||
```
|
||||
|
||||
**Frontend nREPL:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs --frontend '(js/alert "hi")'
|
||||
```
|
||||
|
||||
**Multiple expressions via heredoc (recommended — avoids escaping issues):**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs <<'EOF'
|
||||
(def x 10)
|
||||
(+ x 20)
|
||||
EOF
|
||||
```
|
||||
|
||||
**Override with a different port:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs -p 7888 '(+ 1 2 3)'
|
||||
```
|
||||
|
||||
### Inspect last exception
|
||||
|
||||
After code throws an error, retrieve the full exception details:
|
||||
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs -e
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Require a namespace with reload:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)"
|
||||
```
|
||||
|
||||
**Test a function:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs "(ns/my-function arg1 arg2)"
|
||||
```
|
||||
|
||||
**Long-running operation with custom timeout:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs -t 300000 "(long-running-fn)"
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Default port is 6064** — just pass code directly, no `-p` needed when
|
||||
your nREPL server is on 6064. Use `--backend` (6064) or `--frontend` (3447)
|
||||
as quick aliases. Use `-p <PORT>` for any other port.
|
||||
- **Always use `:reload`** when requiring namespaces to pick up file changes.
|
||||
- **Session is reused** across invocations — defs, in-ns, and var bindings
|
||||
persist. Use `--reset-session` to clear.
|
||||
- **Do not start any server** — the tool connects to an existing nREPL
|
||||
server, it is not the agent's responsibility to start the nREPL server
|
||||
(assume the server is already running on the specified port).
|
||||
44
.serena/memories/tools/taiga.md
Normal file
44
.serena/memories/tools/taiga.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Taiga API client
|
||||
|
||||
`tools/taiga.py` fetches public issues, user stories, and tasks from the
|
||||
Penpot Taiga project (id 345963) without authentication.
|
||||
|
||||
## When to use
|
||||
|
||||
- Fetching details of a Taiga issue, user story, or task by URL or ref number.
|
||||
- Inspecting status, assignee, tags, description, and other metadata.
|
||||
- Piping structured JSON into other scripts (with `--json`).
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
# Fetch by full Taiga URL
|
||||
python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
||||
|
||||
# Fetch by type and ref number
|
||||
python3 tools/taiga.py issue 13714
|
||||
python3 tools/taiga.py us 14128
|
||||
python3 tools/taiga.py task 13648
|
||||
|
||||
# Output raw JSON instead of formatted summary
|
||||
python3 tools/taiga.py --json issue 13714
|
||||
python3 tools/taiga.py --json https://tree.taiga.io/project/penpot/us/14128
|
||||
```
|
||||
|
||||
## Supported types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `issue` | Bug reports, feature requests |
|
||||
| `us` | User stories |
|
||||
| `task` | Implementation tasks |
|
||||
|
||||
## Output
|
||||
|
||||
Default output is a formatted summary with title, status, assignee, author,
|
||||
tags, URL, and description. Use `--json` for the raw API response.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.8+ (standard library only, no dependencies).
|
||||
- Network access to `api.taiga.io`.
|
||||
@ -35,7 +35,7 @@ Command what should be built. Format: `[Imperative verb] [what] in/on [where]`.
|
||||
|
||||
| Field | Rule |
|
||||
|-------|------|
|
||||
| **Labels** | `bug` (crashes/regressions) · `enhancement` (new features) · `community contribution` (PRs from non-core) · skip workflow labels (`backport candidate`, `team-qa`) |
|
||||
| **Labels** | `community contribution` (PRs from non-core) · skip workflow labels (`backport candidate`, `team-qa`) · do **not** add `bug` or `enhancement` labels (use Issue Type instead) |
|
||||
| **Milestone** | Use the current or next planned milestone. Fetch available milestones: `gh api repos/penpot/penpot/milestones --jq '.[].title'`. If unsure, omit. |
|
||||
| **Project** | Always `Main` (project number 8). Use `--project "Main"` flag. |
|
||||
| **Issue Type** | See Issue Type section below. Cannot be set via `gh issue create` — use GraphQL after creation. |
|
||||
@ -114,8 +114,8 @@ Output: `https://github.com/penpot/penpot/issues/<NUMBER>`
|
||||
| Docs | `IT_kwDOAcyBPM4B_IQz` |
|
||||
|
||||
**Map:**
|
||||
- `bug` label → Bug
|
||||
- `enhancement` label → Enhancement
|
||||
- Bug report (steps to reproduce, expected vs. actual) → Bug
|
||||
- Enhancement / new feature → Enhancement
|
||||
- Feature/epic → Feature
|
||||
- Docs → Docs
|
||||
- None of the above → Task
|
||||
|
||||
@ -2,6 +2,20 @@
|
||||
|
||||
PR only on explicit request. Branch: issue/feature-specific; fallback `<type>/<short-description>` (`fix/...`, `feat/...`, `refactor/...`, `docs/...`, `chore/...`, `perf/...`).
|
||||
|
||||
## Target Branch
|
||||
|
||||
Auto-detect the base branch with `tools/detect-target-branch`:
|
||||
|
||||
```bash
|
||||
TARGET=$(tools/detect-target-branch)
|
||||
```
|
||||
|
||||
This outputs `staging` or `develop` by walking the local commit graph (pure local, no remote/network). Do not ask the user for the target branch unless the tool fails.
|
||||
|
||||
## Metadata
|
||||
|
||||
Always add the PR to the Main project (`--project "Main"`) unless the user explicitly requests a different project.
|
||||
|
||||
## Title Format
|
||||
|
||||
PR titles follow commit title conventions:
|
||||
@ -24,7 +38,7 @@ Include concise sections covering:
|
||||
PR descriptions follow this structure:
|
||||
|
||||
```markdown
|
||||
**Note:** This PR was created with AI assistance as part of the Penpot MCP self-improvement initiative.
|
||||
**Note:** This PR was created with AI assistance.
|
||||
|
||||
## What
|
||||
|
||||
@ -60,3 +74,24 @@ The "Note:" line is required at the top. Adjust if this is a manual (non-AI) PR.
|
||||
- Follow `mem:workflow/creating-commits` for commits
|
||||
- Run the focused tests/lints appropriate to touched modules.
|
||||
- Do not force-push during review unless the maintainer workflow explicitly asks for it.
|
||||
- When the user says the code is already pushed, trust that — do not verify remote branch existence via `git ls-remote` or `git fetch`.
|
||||
|
||||
## Creating the PR
|
||||
|
||||
```bash
|
||||
cat > /tmp/pr-body.md << 'PR_BODY'
|
||||
<body content here>
|
||||
PR_BODY
|
||||
|
||||
TARGET=$(tools/detect-target-branch)
|
||||
|
||||
gh pr create \
|
||||
--repo penpot/penpot \
|
||||
--base "$TARGET" \
|
||||
--head <branch> \
|
||||
--title "<title>" \
|
||||
--project "Main" \
|
||||
--body-file /tmp/pr-body.md
|
||||
|
||||
rm -f /tmp/pr-body.md
|
||||
```
|
||||
|
||||
@ -331,7 +331,10 @@
|
||||
|
||||
This expands to a single SQL statement with placeholders for every
|
||||
value being inserted. For large data sets, this may exceed the limit
|
||||
of sql string size and/or number of parameters."
|
||||
of sql string size and/or number of parameters.
|
||||
|
||||
See `insert-many-chunked!` for a safe alternative that automatically
|
||||
partitions rows to stay within the parameter limit."
|
||||
[ds table cols rows & {:as opts}]
|
||||
(let [conn (get-connectable ds)
|
||||
sql (sql/insert-many table cols rows opts)
|
||||
@ -341,6 +344,24 @@
|
||||
opts (update opts :return-keys boolean)]
|
||||
(jdbc/execute! conn sql opts)))
|
||||
|
||||
(def ^:private default-max-params
|
||||
"PostgreSQL PreparedStatement parameter limit."
|
||||
65535)
|
||||
|
||||
(defn insert-many-chunked!
|
||||
"Like `insert-many!` but partitions rows into chunks that stay within
|
||||
PostgreSQL's 65,535 PreparedStatement parameter limit.
|
||||
|
||||
The chunk size is computed as `floor(max-params / num-columns)`,
|
||||
so callers do not need to calculate it. All chunks execute within
|
||||
the same transaction when called inside `tx-run!`."
|
||||
[ds table cols rows & {:keys [max-params] :as opts
|
||||
:or {max-params default-max-params}}]
|
||||
(let [chunk-size (quot max-params (count cols))
|
||||
opts (dissoc opts :max-params)]
|
||||
(doseq [chunk (partition-all chunk-size rows)]
|
||||
(apply insert-many! ds table cols chunk (mapcat identity opts)))))
|
||||
|
||||
(defn update!
|
||||
"A helper that build an UPDATE SQL statement and executes it.
|
||||
|
||||
|
||||
@ -65,7 +65,6 @@
|
||||
(assert (every? string? cmd) "the command should be a vector of strings")
|
||||
|
||||
(let [executor (::wrk/executor system)
|
||||
_ (assert (some? executor) "executor is required, check ::wrk/executor")
|
||||
full-cmd (cond->> cmd
|
||||
(seq prlimit)
|
||||
(into (prlimit-cmd prlimit)))
|
||||
@ -74,6 +73,9 @@
|
||||
_ (reduce-kv set-env env-map env)
|
||||
process (.start builder)]
|
||||
|
||||
(when-not executor
|
||||
(throw (IllegalArgumentException. "invalid system/cfg provided, missing ::wrk/executor")))
|
||||
|
||||
(if in
|
||||
(px/run! executor
|
||||
(fn []
|
||||
|
||||
@ -8,12 +8,17 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.util.shell :as shell]
|
||||
[app.worker :as-alias wrk]
|
||||
[clojure.string :as str]
|
||||
[clojure.test :as t]))
|
||||
[clojure.test :as t]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(def ^:private system
|
||||
{::wrk/executor (px/cached-executor)})
|
||||
|
||||
(t/deftest exec-normal-completes
|
||||
(t/testing "normal process completes within timeout"
|
||||
(let [result (shell/exec! {}
|
||||
(let [result (shell/exec! system
|
||||
:cmd ["echo" "hello"]
|
||||
:timeout 10)]
|
||||
(t/is (= 0 (:exit result)))
|
||||
@ -21,7 +26,7 @@
|
||||
|
||||
(t/deftest exec-captures-stderr
|
||||
(t/testing "stderr is captured separately"
|
||||
(let [result (shell/exec! {}
|
||||
(let [result (shell/exec! system
|
||||
:cmd ["bash" "-c" "echo out; echo err >&2"]
|
||||
:timeout 10)]
|
||||
(t/is (= 0 (:exit result)))
|
||||
@ -30,14 +35,14 @@
|
||||
|
||||
(t/deftest exec-non-zero-exit
|
||||
(t/testing "non-zero exit code is captured"
|
||||
(let [result (shell/exec! {}
|
||||
(let [result (shell/exec! system
|
||||
:cmd ["bash" "-c" "exit 42"]
|
||||
:timeout 10)]
|
||||
(t/is (= 42 (:exit result))))))
|
||||
|
||||
(t/deftest exec-with-env
|
||||
(t/testing "environment variables are passed to the process"
|
||||
(let [result (shell/exec! {}
|
||||
(let [result (shell/exec! system
|
||||
:cmd ["bash" "-c" "echo $MY_VAR"]
|
||||
:env {"MY_VAR" "test-value"}
|
||||
:timeout 10)]
|
||||
@ -46,7 +51,7 @@
|
||||
|
||||
(t/deftest exec-with-input
|
||||
(t/testing "stdin input is passed to the process"
|
||||
(let [result (shell/exec! {}
|
||||
(let [result (shell/exec! system
|
||||
:cmd ["cat"]
|
||||
:in "hello from stdin"
|
||||
:timeout 10)]
|
||||
@ -57,7 +62,7 @@
|
||||
(t/testing "process that exceeds timeout is killed and raises exception"
|
||||
(let [start (System/currentTimeMillis)]
|
||||
(try
|
||||
(shell/exec! {}
|
||||
(shell/exec! system
|
||||
:cmd ["sleep" "60"]
|
||||
:timeout 1)
|
||||
(t/is false "should have thrown")
|
||||
@ -72,14 +77,14 @@
|
||||
|
||||
(t/deftest exec-no-timeout-waits
|
||||
(t/testing "without timeout, process runs to completion"
|
||||
(let [result (shell/exec! {}
|
||||
(let [result (shell/exec! system
|
||||
:cmd ["sleep" "0.1"]
|
||||
:timeout nil)]
|
||||
(t/is (= 0 (:exit result))))))
|
||||
|
||||
(t/deftest exec-prlimit-normal
|
||||
(t/testing "normal process completes within prlimit"
|
||||
(let [result (shell/exec! {}
|
||||
(let [result (shell/exec! system
|
||||
:cmd ["echo" "hello"]
|
||||
:prlimit {:mem 256 :cpu 10}
|
||||
:timeout 10)]
|
||||
@ -88,7 +93,7 @@
|
||||
|
||||
(t/deftest exec-prlimit-cpu
|
||||
(t/testing "process exceeding CPU limit is killed"
|
||||
(let [result (shell/exec! {}
|
||||
(let [result (shell/exec! system
|
||||
:cmd ["bash" "-c" "while true; do :; done"]
|
||||
:prlimit {:cpu 2}
|
||||
:timeout 10)]
|
||||
@ -98,7 +103,7 @@
|
||||
(t/testing "process exceeding memory limit is killed"
|
||||
;; Use python3 to allocate more memory than the limit allows.
|
||||
;; This test requires python3 to be available in the environment.
|
||||
(let [result (shell/exec! {}
|
||||
(let [result (shell/exec! system
|
||||
:cmd ["python3" "-c"
|
||||
"import sys; x = bytearray(600 * 1024 * 1024); sys.exit(0)"]
|
||||
:prlimit {:mem 256}
|
||||
|
||||
@ -500,9 +500,9 @@
|
||||
shape-transform (:transform shape)
|
||||
shape-transform-inv (:transform-inverse shape)
|
||||
shape-center (gco/shape->center shape)
|
||||
{sr-width :width sr-height :height} (:selrect shape)
|
||||
{sr-width :width sr-height :height} (safe-size-rect shape)
|
||||
|
||||
origin (cond-> (gpt/point (:selrect shape))
|
||||
origin (cond-> (gpt/point (safe-size-rect shape))
|
||||
(some? shape-transform)
|
||||
(gmt/transform-point-center shape-center shape-transform))
|
||||
|
||||
|
||||
@ -392,7 +392,9 @@
|
||||
([shape]
|
||||
(convert-to-path shape {}))
|
||||
([shape objects]
|
||||
(-> (stp/convert-to-path shape objects)
|
||||
(update :content impl/path-data))))
|
||||
(let [shape' (stp/convert-to-path shape objects)]
|
||||
(if (identical? shape shape')
|
||||
shape'
|
||||
(update shape' :content impl/path-data)))))
|
||||
|
||||
(dm/export impl/decode-segments)
|
||||
|
||||
@ -657,3 +657,35 @@
|
||||
mods (ctm/rotation (ctm/empty) (gpt/point 50 25) 45)
|
||||
result (ctm/apply-structure-modifiers shape mods)]
|
||||
(t/is (mth/close? 45.0 (:rotation result))))))
|
||||
|
||||
;; ─── change-orientation-modifiers — degenerate selrect ────────────────────────
|
||||
|
||||
(defn- make-degenerate-shape
|
||||
"Build a shape whose selrect has zero width/height, simulating a shape
|
||||
decoded from the server via map->Rect (bypasses make-rect's 0.01 floor)."
|
||||
[x y selrect-width selrect-height]
|
||||
(let [shape (make-shape x y 100 50)]
|
||||
(assoc shape :selrect (grc/map->Rect {:x x :y y
|
||||
:width selrect-width
|
||||
:height selrect-height
|
||||
:x1 x :y1 y
|
||||
:x2 (+ x selrect-width)
|
||||
:y2 (+ y selrect-height)}))))
|
||||
|
||||
(t/deftest change-orientation-zero-width-selrect-does-not-throw
|
||||
(t/testing "orientation change on a shape with zero selrect width does not throw"
|
||||
(let [shape (make-degenerate-shape 0 0 0 50)
|
||||
mods (ctm/change-orientation-modifiers shape :horiz)]
|
||||
(t/is (some? mods)))))
|
||||
|
||||
(t/deftest change-orientation-zero-height-selrect-does-not-throw
|
||||
(t/testing "orientation change on a shape with zero selrect height does not throw"
|
||||
(let [shape (make-degenerate-shape 0 0 100 0)
|
||||
mods (ctm/change-orientation-modifiers shape :vert)]
|
||||
(t/is (some? mods)))))
|
||||
|
||||
(t/deftest change-orientation-zero-width-and-height-selrect-does-not-throw
|
||||
(t/testing "orientation change on a fully degenerate selrect does not throw"
|
||||
(let [shape (make-degenerate-shape 0 0 0 0)
|
||||
mods (ctm/change-orientation-modifiers shape :horiz)]
|
||||
(t/is (some? mods)))))
|
||||
|
||||
@ -1370,6 +1370,15 @@
|
||||
;; A path shape stays a path shape unchanged
|
||||
(t/is (= :path (:type result)))))
|
||||
|
||||
(t/deftest shape-to-path-svg-raw-does-not-throw
|
||||
(let [shape {:type :svg-raw :x 0.0 :y 0.0 :width 100.0 :height 50.0
|
||||
:selrect (make-selrect 0.0 0.0 100.0 50.0)
|
||||
:content {:tag :text :attrs {:style {}}
|
||||
:content [{:tag :tspan :attrs {} :content ["x"]}]}}
|
||||
result (path/convert-to-path shape {})]
|
||||
(t/is (= :svg-raw (:type result)))
|
||||
(t/is (some? (:content result)))))
|
||||
|
||||
(t/deftest shape-to-path-rect-with-radius
|
||||
(let [shape {:type :rect :x 0.0 :y 0.0 :width 100.0 :height 100.0
|
||||
:r1 10.0 :r2 10.0 :r3 10.0 :r4 10.0
|
||||
|
||||
@ -66,7 +66,7 @@ RUN set -eux; \
|
||||
|
||||
FROM base AS setup-opencode
|
||||
|
||||
ENV OPENCODE_VERSION=1.17.13
|
||||
ENV OPENCODE_VERSION=1.17.16
|
||||
|
||||
RUN set -ex; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
|
||||
@ -197,10 +197,10 @@ services:
|
||||
- penpot
|
||||
|
||||
environment:
|
||||
<< : [*penpot-secret-key]
|
||||
<< : [*penpot-secret-key, *penpot-public-uri]
|
||||
# Don't touch it; this uses an internal docker network to
|
||||
# communicate with the frontend.
|
||||
PENPOT_PUBLIC_URI: http://penpot-frontend:8080
|
||||
PENPOT_INTERNAL_URI: http://penpot-frontend:8080
|
||||
|
||||
## Valkey (or previously Redis) is used for the websockets notifications.
|
||||
PENPOT_REDIS_URI: redis://penpot-valkey/0
|
||||
|
||||
@ -651,6 +651,21 @@ PENPOT_EXPORTER_URI: http://your-penpot-exporter:6061
|
||||
|
||||
These variables are used for generate correct nginx.conf file on container startup.
|
||||
|
||||
### Exporter
|
||||
|
||||
The exporter uses this variable:
|
||||
|
||||
```bash
|
||||
# Exporter
|
||||
PENPOT_INTERNAL_URI: http://penpot-frontend:8080
|
||||
```
|
||||
|
||||
- `PENPOT_INTERNAL_URI`: The URI used by the exporter's headless browser to
|
||||
communicate with the frontend (internal Docker network). Defaults to
|
||||
`PENPOT_PUBLIC_URI` if not set. The default value
|
||||
`http://penpot-frontend:8080` used in the docker-compose is a good default and
|
||||
it is recommended to keep it unchanged.
|
||||
|
||||
## Other flags
|
||||
|
||||
There are other flags that are useful for a more customized Penpot experience. This section has the list of the flags meant
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
|
||||
(def ^:private defaults
|
||||
{:public-uri "http://localhost:3449"
|
||||
:internal-uri nil
|
||||
:tenant "default"
|
||||
:host "localhost"
|
||||
:http-server-port 6061
|
||||
@ -33,6 +34,7 @@
|
||||
[:map {:title "config"}
|
||||
[:secret-key :string]
|
||||
[:public-uri {:optional true} ::sm/uri]
|
||||
[:internal-uri {:optional true} ::sm/uri]
|
||||
[:exporter-shared-key {:optional true} :string]
|
||||
[:host {:optional true} :string]
|
||||
[:tenant {:optional true} :string]
|
||||
@ -100,6 +102,12 @@
|
||||
([key default]
|
||||
(c/get config key default)))
|
||||
|
||||
(defn get-internal-uri
|
||||
"Returns internal-uri if set, otherwise falls back to public-uri."
|
||||
[]
|
||||
(or (c/get config :internal-uri)
|
||||
(c/get config :public-uri)))
|
||||
|
||||
(def management-key
|
||||
(let [key (or (c/get config :exporter-shared-key)
|
||||
(let [secret-key (c/get config :secret-key)
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
[& _]
|
||||
(l/info :msg "initializing"
|
||||
:public-uri (str (cf/get :public-uri))
|
||||
:internal-uri (str (cf/get-internal-uri))
|
||||
:version (:full cf/version))
|
||||
(p/do!
|
||||
(bwr/init)
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
:method "POST"
|
||||
:body fdata
|
||||
:dispatcher agent}
|
||||
uri (-> (cf/get :public-uri)
|
||||
uri (-> (cf/get-internal-uri)
|
||||
(u/ensure-path-slash)
|
||||
(u/join "api/management/methods/upload-tempfile")
|
||||
(str))]
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
:skip-children skip-children
|
||||
:wasm (when is-wasm "true")
|
||||
:scale scale}
|
||||
uri (-> (cf/get :public-uri)
|
||||
uri (-> (cf/get-internal-uri)
|
||||
(u/ensure-path-slash)
|
||||
(u/join "render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
@ -77,7 +77,7 @@
|
||||
(on-object (assoc object :path path))
|
||||
(p/recur (rest objects))))))]
|
||||
|
||||
(let [base-uri (-> (cf/get :public-uri)
|
||||
(let [base-uri (-> (cf/get-internal-uri)
|
||||
(u/ensure-path-slash))]
|
||||
(bw/exec! (prepare-options base-uri)
|
||||
(partial render base-uri)))))
|
||||
|
||||
@ -108,6 +108,18 @@
|
||||
{:width width
|
||||
:height height}))
|
||||
|
||||
(defn- replace-internal-uris
|
||||
"Replaces internal-uri references with public-uri in SVG output.
|
||||
This ensures that font URLs and other resource references in the
|
||||
exported SVG use the public-facing URI accessible to end users."
|
||||
[svg-content]
|
||||
(let [internal-uri (str (cf/get-internal-uri))
|
||||
public-uri (str (cf/get :public-uri))]
|
||||
(if (and (not= internal-uri public-uri)
|
||||
(str/includes? svg-content internal-uri))
|
||||
(str/replace svg-content internal-uri public-uri)
|
||||
svg-content)))
|
||||
|
||||
(defn render
|
||||
[{:keys [page-id file-id share-id objects token scale type]} on-object]
|
||||
(letfn [(convert-to-ppm [pngpath]
|
||||
@ -320,7 +332,9 @@
|
||||
|
||||
result (if (contains? cf/flags :exporter-svgo)
|
||||
(svgo/optimize result svgo/defaultOptions)
|
||||
result)]
|
||||
result)
|
||||
|
||||
result (replace-internal-uris result)]
|
||||
|
||||
;; (println "------- ORIGIN:")
|
||||
;; (cljs.pprint/pprint (xml->clj xmldata))
|
||||
@ -349,7 +363,7 @@
|
||||
:render-embed true
|
||||
:object-id (mapv :id objects)
|
||||
:route "objects"}
|
||||
uri (-> (cf/get :public-uri)
|
||||
uri (-> (cf/get-internal-uri)
|
||||
(u/ensure-path-slash)
|
||||
(u/join "render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"author": "Andrey Antukh",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"draft-js": "penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0",
|
||||
"draft-js": "penpot/draft-js.git#ba3b26ed63a01227a3560e440531b69d79c03f35",
|
||||
"immutable": "^5.1.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
449
frontend/packages/draft-js/pnpm-lock.yaml
generated
449
frontend/packages/draft-js/pnpm-lock.yaml
generated
@ -1,449 +0,0 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
draft-js:
|
||||
specifier: penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0
|
||||
version: https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
immutable:
|
||||
specifier: ^5.1.4
|
||||
version: 5.1.4
|
||||
react:
|
||||
specifier: '>=0.17.0'
|
||||
version: 19.2.3
|
||||
react-dom:
|
||||
specifier: '>=0.17.0'
|
||||
version: 19.2.3(react@19.2.3)
|
||||
devDependencies:
|
||||
esbuild:
|
||||
specifier: ^0.27.2
|
||||
version: 0.27.2
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
asap@2.0.6:
|
||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||
|
||||
cross-fetch@3.2.0:
|
||||
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
|
||||
|
||||
draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0:
|
||||
resolution: {tarball: https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0}
|
||||
version: 0.11.7
|
||||
peerDependencies:
|
||||
react: '>=0.14.0'
|
||||
react-dom: '>=0.14.0'
|
||||
|
||||
esbuild@0.27.2:
|
||||
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
fbjs-css-vars@1.0.2:
|
||||
resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==}
|
||||
|
||||
fbjs@3.0.5:
|
||||
resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==}
|
||||
|
||||
immutable@3.7.6:
|
||||
resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
||||
immutable@5.1.4:
|
||||
resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
promise@7.3.1:
|
||||
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
|
||||
|
||||
react-dom@19.2.3:
|
||||
resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
|
||||
peerDependencies:
|
||||
react: ^19.2.3
|
||||
|
||||
react@19.2.3:
|
||||
resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
setimmediate@1.0.5:
|
||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
ua-parser-js@1.0.41:
|
||||
resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
|
||||
hasBin: true
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
asap@2.0.6: {}
|
||||
|
||||
cross-fetch@3.2.0:
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
fbjs: 3.0.5
|
||||
immutable: 3.7.6
|
||||
object-assign: 4.1.1
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
esbuild@0.27.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.2
|
||||
'@esbuild/android-arm': 0.27.2
|
||||
'@esbuild/android-arm64': 0.27.2
|
||||
'@esbuild/android-x64': 0.27.2
|
||||
'@esbuild/darwin-arm64': 0.27.2
|
||||
'@esbuild/darwin-x64': 0.27.2
|
||||
'@esbuild/freebsd-arm64': 0.27.2
|
||||
'@esbuild/freebsd-x64': 0.27.2
|
||||
'@esbuild/linux-arm': 0.27.2
|
||||
'@esbuild/linux-arm64': 0.27.2
|
||||
'@esbuild/linux-ia32': 0.27.2
|
||||
'@esbuild/linux-loong64': 0.27.2
|
||||
'@esbuild/linux-mips64el': 0.27.2
|
||||
'@esbuild/linux-ppc64': 0.27.2
|
||||
'@esbuild/linux-riscv64': 0.27.2
|
||||
'@esbuild/linux-s390x': 0.27.2
|
||||
'@esbuild/linux-x64': 0.27.2
|
||||
'@esbuild/netbsd-arm64': 0.27.2
|
||||
'@esbuild/netbsd-x64': 0.27.2
|
||||
'@esbuild/openbsd-arm64': 0.27.2
|
||||
'@esbuild/openbsd-x64': 0.27.2
|
||||
'@esbuild/openharmony-arm64': 0.27.2
|
||||
'@esbuild/sunos-x64': 0.27.2
|
||||
'@esbuild/win32-arm64': 0.27.2
|
||||
'@esbuild/win32-ia32': 0.27.2
|
||||
'@esbuild/win32-x64': 0.27.2
|
||||
|
||||
fbjs-css-vars@1.0.2: {}
|
||||
|
||||
fbjs@3.0.5:
|
||||
dependencies:
|
||||
cross-fetch: 3.2.0
|
||||
fbjs-css-vars: 1.0.2
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
promise: 7.3.1
|
||||
setimmediate: 1.0.5
|
||||
ua-parser-js: 1.0.41
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
immutable@3.7.6: {}
|
||||
|
||||
immutable@5.1.4: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
promise@7.3.1:
|
||||
dependencies:
|
||||
asap: 2.0.6
|
||||
|
||||
react-dom@19.2.3(react@19.2.3):
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
scheduler: 0.27.0
|
||||
|
||||
react@19.2.3: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
ua-parser-js@1.0.41: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
10
frontend/pnpm-lock.yaml
generated
10
frontend/pnpm-lock.yaml
generated
@ -263,8 +263,8 @@ importers:
|
||||
packages/draft-js:
|
||||
dependencies:
|
||||
draft-js:
|
||||
specifier: penpot/draft-js.git#4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0
|
||||
version: https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0(encoding@0.1.13)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
specifier: penpot/draft-js.git#ba3b26ed63a01227a3560e440531b69d79c03f35
|
||||
version: https://codeload.github.com/penpot/draft-js/tar.gz/ba3b26ed63a01227a3560e440531b69d79c03f35(encoding@0.1.13)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
immutable:
|
||||
specifier: ^5.1.9
|
||||
version: 5.1.9
|
||||
@ -2886,8 +2886,8 @@ packages:
|
||||
resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0:
|
||||
resolution: {gitHosted: true, tarball: https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0}
|
||||
draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/ba3b26ed63a01227a3560e440531b69d79c03f35:
|
||||
resolution: {gitHosted: true, integrity: sha512-IN2r8sw36jcH32WPP9eBFXNglirhkFIsqgoKzwwSDxiCbeipumeKXjYB3vw82CxYRwKc+oRTMcZ5jNnCWnmnBw==, tarball: https://codeload.github.com/penpot/draft-js/tar.gz/ba3b26ed63a01227a3560e440531b69d79c03f35}
|
||||
version: 0.11.7
|
||||
peerDependencies:
|
||||
react: '>=0.14.0'
|
||||
@ -8117,7 +8117,7 @@ snapshots:
|
||||
domelementtype: 3.0.0
|
||||
domhandler: 6.0.1
|
||||
|
||||
draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0(encoding@0.1.13)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/ba3b26ed63a01227a3560e440531b69d79c03f35(encoding@0.1.13)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
dependencies:
|
||||
fbjs: 3.0.5(encoding@0.1.13)
|
||||
immutable: 3.8.3
|
||||
|
||||
@ -24,7 +24,6 @@
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.edition :as dwe]
|
||||
[app.main.data.workspace.layout :as dwlo]
|
||||
[app.main.data.workspace.selection :as dws]
|
||||
[app.main.data.workspace.zoom :as dwz]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
@ -71,7 +70,7 @@
|
||||
|
||||
(rx/take-until stopper-s))))))
|
||||
|
||||
(defn- handle-interrupt
|
||||
(defn handle-interrupt
|
||||
[]
|
||||
(ptk/reify ::handle-interrupt
|
||||
ptk/WatchEvent
|
||||
@ -85,8 +84,7 @@
|
||||
;; tool is active. When comments are merely visible during design,
|
||||
;; `select-shape` emits `:interrupt` and this would otherwise wipe
|
||||
;; the freshly selected shape, breaking click selection.
|
||||
comments-mode? (rx/of (dwe/clear-edition-mode)
|
||||
(dws/deselect-all true))
|
||||
comments-mode? (rx/of (dwe/clear-edition-mode))
|
||||
:else (rx/empty))))))
|
||||
|
||||
;; Event responsible of the what should be executed when user clicked
|
||||
|
||||
@ -256,8 +256,8 @@
|
||||
|
||||
(defn assoc-position-data
|
||||
[shape position-data old-shape]
|
||||
(let [deltav (gpt/to-vec (gpt/point (:selrect old-shape))
|
||||
(gpt/point (:selrect shape)))
|
||||
(let [deltav (gpt/to-vec (gpt/point (ctm/safe-size-rect old-shape))
|
||||
(gpt/point (ctm/safe-size-rect shape)))
|
||||
position-data
|
||||
(-> position-data
|
||||
(gsh/move-position-data deltav))]
|
||||
|
||||
@ -150,7 +150,7 @@
|
||||
.separator {
|
||||
margin: 0;
|
||||
block-size: $sz-12;
|
||||
border-block-start: $b-1 solid var(--color-background-primary);
|
||||
border-block-start: $b-1 solid var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
&[data-direction="up"] {
|
||||
|
||||
@ -35,8 +35,8 @@
|
||||
(if-let [modifiers (:modifiers shape)]
|
||||
(let [shape' (gsh/transform-shape shape modifiers)
|
||||
|
||||
old-sr (dm/get-prop shape :selrect)
|
||||
new-sr (dm/get-prop shape' :selrect)
|
||||
old-sr (ctm/safe-size-rect shape)
|
||||
new-sr (ctm/safe-size-rect shape')
|
||||
|
||||
;; We need to remove the movement because the dynamic modifiers will have move it
|
||||
deltav (gpt/to-vec (gpt/point new-sr)
|
||||
|
||||
@ -522,7 +522,7 @@
|
||||
[:*
|
||||
[:div {:class (stl/css :variant-property-list)}
|
||||
(for [[pos prop] (map-indexed vector props-first)]
|
||||
(let [mixed-value? (not-every? #(= (:value prop) (:value (nth % pos))) properties)
|
||||
(let [mixed-value? (not-every? #(= (:value prop) (:value (get % pos))) properties)
|
||||
options (get-options (:name prop))
|
||||
boolean-pair (ctv/find-boolean-pair (mapv :id options))
|
||||
options (cond-> options
|
||||
|
||||
@ -171,12 +171,20 @@
|
||||
on-caps-start-change
|
||||
(mf/use-fn
|
||||
(mf/deps index on-stroke-cap-start-change)
|
||||
#(on-stroke-cap-start-change index (keyword %)))
|
||||
(fn [cap]
|
||||
(let [cap (if (= cap "none")
|
||||
nil
|
||||
(keyword cap))]
|
||||
(on-stroke-cap-start-change index cap))))
|
||||
|
||||
on-caps-end-change
|
||||
(mf/use-fn
|
||||
(mf/deps index on-stroke-cap-end-change)
|
||||
#(on-stroke-cap-end-change index (keyword %)))
|
||||
(fn [cap]
|
||||
(let [cap (if (= cap "none")
|
||||
nil
|
||||
(keyword cap))]
|
||||
(on-stroke-cap-end-change index cap))))
|
||||
|
||||
on-detach-token-color
|
||||
(mf/use-fn
|
||||
@ -191,16 +199,16 @@
|
||||
(on-detach-token token #{:stroke-width})))
|
||||
|
||||
stroke-caps-options
|
||||
[{:value nil :label (tr "workspace.options.stroke-cap.none")}
|
||||
:separator
|
||||
{:value :line-arrow :label (tr "workspace.options.stroke-cap.line-arrow-short") :icon :stroke-arrow}
|
||||
{:value :triangle-arrow :label (tr "workspace.options.stroke-cap.triangle-arrow-short") :icon :stroke-triangle}
|
||||
{:value :square-marker :label (tr "workspace.options.stroke-cap.square-marker-short") :icon :stroke-rectangle}
|
||||
{:value :circle-marker :label (tr "workspace.options.stroke-cap.circle-marker-short") :icon :stroke-circle}
|
||||
{:value :diamond-marker :label (tr "workspace.options.stroke-cap.diamond-marker-short") :icon :stroke-diamond}
|
||||
:separator
|
||||
{:value :round :label (tr "workspace.options.stroke-cap.round") :icon :stroke-rounded}
|
||||
{:value :square :label (tr "workspace.options.stroke-cap.square") :icon :stroke-squared}]
|
||||
[{:id "none" :value "none" :label (tr "workspace.options.stroke-cap.none")}
|
||||
{:label "" :type :separator :id "separator"}
|
||||
{:id "line-arrow" :value :line-arrow :label (tr "workspace.options.stroke-cap.line-arrow-short") :icon i/stroke-arrow}
|
||||
{:id "triangle-arrow" :value :triangle-arrow :label (tr "workspace.options.stroke-cap.triangle-arrow-short") :icon i/stroke-triangle}
|
||||
{:id "square-marker" :value :square-marker :label (tr "workspace.options.stroke-cap.square-marker-short") :icon i/stroke-rectangle}
|
||||
{:id "circle-marker" :value :circle-marker :label (tr "workspace.options.stroke-cap.circle-marker-short") :icon i/stroke-circle}
|
||||
{:id "diamond-marker" :value :diamond-marker :label (tr "workspace.options.stroke-cap.diamond-marker-short") :icon i/stroke-diamond}
|
||||
{:label "" :type :separator :id "separator"}
|
||||
{:id "round" :value :round :label (tr "workspace.options.stroke-cap.round") :icon i/stroke-rounded}
|
||||
{:id "square" :value :square :label (tr "workspace.options.stroke-cap.square") :icon i/stroke-squared}]
|
||||
|
||||
on-cap-switch
|
||||
(mf/use-fn
|
||||
@ -340,16 +348,18 @@
|
||||
;; Stroke Caps
|
||||
(when show-caps
|
||||
[:div {:class (stl/css :stroke-caps-options)}
|
||||
[:& select {:default-value (:stroke-cap-start stroke)
|
||||
:options stroke-caps-options
|
||||
:disabled hidden?
|
||||
:on-change on-caps-start-change}]
|
||||
[:> select* {:default-selected (or (d/name (:stroke-cap-start stroke)) "none")
|
||||
:options stroke-caps-options
|
||||
:data-testid "stroke.cap-start"
|
||||
:disabled hidden?
|
||||
:on-change on-caps-start-change}]
|
||||
[:> icon-button* {:variant "secondary"
|
||||
:aria-label (tr "labels.switch")
|
||||
:disabled hidden?
|
||||
:on-click on-cap-switch
|
||||
:icon i/switch}]
|
||||
[:& select {:default-value (:stroke-cap-end stroke)
|
||||
:options stroke-caps-options
|
||||
:disabled hidden?
|
||||
:on-change on-caps-end-change}]])]))
|
||||
[:> select* {:default-selected (or (d/name (:stroke-cap-end stroke)) "none")
|
||||
:options stroke-caps-options
|
||||
:data-testid "stroke.cap-end"
|
||||
:disabled hidden?
|
||||
:on-change on-caps-end-change}]])]))
|
||||
|
||||
@ -1059,6 +1059,9 @@
|
||||
:setPluginData
|
||||
(fn [key value]
|
||||
(cond
|
||||
(not= file-id (:current-file-id @st/state))
|
||||
(u/not-valid plugin-id :setPluginData-non-local-library file-id)
|
||||
|
||||
(not (string? key))
|
||||
(u/not-valid plugin-id :setPluginData-key key)
|
||||
|
||||
@ -1092,6 +1095,9 @@
|
||||
:setSharedPluginData
|
||||
(fn [namespace key value]
|
||||
(cond
|
||||
(not= file-id (:current-file-id @st/state))
|
||||
(u/not-valid plugin-id :setSharedPluginData-non-local-library file-id)
|
||||
|
||||
(not (string? namespace))
|
||||
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||
|
||||
|
||||
@ -272,13 +272,13 @@
|
||||
(st/emit! (dp/set-plugin-data file-id :page id (keyword "shared" namespace) key value))))
|
||||
|
||||
:getSharedPluginDataKeys
|
||||
(fn [self namespace]
|
||||
(fn [namespace]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/not-valid plugin-id :page-plugin-data-namespace namespace)
|
||||
|
||||
:else
|
||||
(let [page (u/proxy->page self)]
|
||||
(let [page (u/locate-page file-id id)]
|
||||
(apply array (keys (dm/get-in page [:plugin-data (keyword "shared" namespace)]))))))
|
||||
|
||||
:openPage
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
;; 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 frontend-tests.data.workspace-comments-test
|
||||
(:require
|
||||
[app.main.data.comments :as dcmt]
|
||||
[app.main.data.workspace.comments :as dwcm]
|
||||
[app.main.data.workspace.edition :as dwe]
|
||||
[beicon.v2.core :as rx]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(t/deftest test-handle-interrupt-draft
|
||||
(t/async
|
||||
done
|
||||
(let [event (dwcm/handle-interrupt)
|
||||
state {:comments-local {:draft {:id "draft-id"}}}
|
||||
result (ptk/watch event state (rx/empty))]
|
||||
(->> result
|
||||
(rx/subs!
|
||||
(fn [evt]
|
||||
(t/is (= ::dcmt/close-comment-thread (ptk/type evt))))
|
||||
(fn [err]
|
||||
(done)
|
||||
(js/console.error err)
|
||||
(t/do-report {:type :error :message "Stream error" :actual err}))
|
||||
(fn [_]
|
||||
(done)))))))
|
||||
|
||||
(t/deftest test-handle-interrupt-open
|
||||
(t/async
|
||||
done
|
||||
(let [event (dwcm/handle-interrupt)
|
||||
state {:comments-local {:open {:id "thread-id"}}}
|
||||
result (ptk/watch event state (rx/empty))]
|
||||
(->> result
|
||||
(rx/subs!
|
||||
(fn [evt]
|
||||
(t/is (= ::dcmt/close-comment-thread (ptk/type evt))))
|
||||
(fn [err]
|
||||
(done)
|
||||
(js/console.error err)
|
||||
(t/do-report {:type :error :message "Stream error" :actual err}))
|
||||
(fn [_]
|
||||
(done)))))))
|
||||
|
||||
(t/deftest test-handle-interrupt-comments-mode
|
||||
(t/async
|
||||
done
|
||||
(let [event (dwcm/handle-interrupt)
|
||||
state {:workspace-drawing {:tool :comments}}
|
||||
result (ptk/watch event state (rx/empty))]
|
||||
(->> result
|
||||
(rx/subs!
|
||||
(fn [evt]
|
||||
(t/is (= ::dwe/clear-edition-mode (ptk/type evt))))
|
||||
(fn [err]
|
||||
(done)
|
||||
(js/console.error err)
|
||||
(t/do-report {:type :error :message "Stream error" :actual err}))
|
||||
(fn [_]
|
||||
(done)))))))
|
||||
|
||||
(t/deftest test-handle-interrupt-noop
|
||||
(t/async
|
||||
done
|
||||
(let [event (dwcm/handle-interrupt)
|
||||
state {}
|
||||
result (ptk/watch event state (rx/empty))
|
||||
emitted? (atom false)]
|
||||
(->> result
|
||||
(rx/subs!
|
||||
(fn [_]
|
||||
(reset! emitted? true))
|
||||
(fn [err]
|
||||
(done)
|
||||
(js/console.error err)
|
||||
(t/do-report {:type :error :message "Stream error" :actual err}))
|
||||
(fn [_]
|
||||
(t/is (false? @emitted?) "should not emit any events")
|
||||
(done)))))))
|
||||
@ -9,9 +9,11 @@
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.test-helpers.files :as cthf]
|
||||
[app.common.test-helpers.shapes :as cths]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.text :as txt]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.ui.workspace.shapes.text.viewport-texts-html :as vth]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[frontend-tests.helpers.state :as ths]))
|
||||
|
||||
@ -374,3 +376,34 @@
|
||||
"exactly one typography was added")
|
||||
(t/is (= "0.1" (:letter-spacing (first typographies)))
|
||||
"float letter-spacing is normalised to 2-decimal string")))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tests: fix-position with degenerate selrect
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest fix-position-zero-width-selrect-does-not-throw
|
||||
(t/testing "fix-position on a shape with zero selrect width does not throw"
|
||||
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 50)
|
||||
modifiers (ctm/change-dimensions-modifiers shape :width 200 {:ignore-lock? true})
|
||||
shape' (assoc shape :modifiers modifiers)
|
||||
result (vth/fix-position shape')]
|
||||
(t/is (some? result))
|
||||
(t/is (some? (:selrect result))))))
|
||||
|
||||
(t/deftest fix-position-zero-height-selrect-does-not-throw
|
||||
(t/testing "fix-position on a shape with zero selrect height does not throw"
|
||||
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 100 :height 0)
|
||||
modifiers (ctm/change-dimensions-modifiers shape :height 80 {:ignore-lock? true})
|
||||
shape' (assoc shape :modifiers modifiers)
|
||||
result (vth/fix-position shape')]
|
||||
(t/is (some? result))
|
||||
(t/is (some? (:selrect result))))))
|
||||
|
||||
(t/deftest fix-position-zero-width-and-height-selrect-does-not-throw
|
||||
(t/testing "fix-position on a fully degenerate selrect does not throw"
|
||||
(let [shape (make-degenerate-text-shape :x 0 :y 0 :width 0 :height 0)
|
||||
modifiers (ctm/change-dimensions-modifiers shape :width 150 {:ignore-lock? true})
|
||||
shape' (assoc shape :modifiers modifiers)
|
||||
result (vth/fix-position shape')]
|
||||
(t/is (some? result))
|
||||
(t/is (some? (:selrect result))))))
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
[frontend-tests.data.uploads-test]
|
||||
[frontend-tests.data.viewer-test]
|
||||
[frontend-tests.data.workspace-colors-test]
|
||||
[frontend-tests.data.workspace-comments-test]
|
||||
[frontend-tests.data.workspace-interactions-test]
|
||||
[frontend-tests.data.workspace-mcp-test]
|
||||
[frontend-tests.data.workspace-media-test]
|
||||
@ -85,6 +86,7 @@
|
||||
'frontend-tests.data.uploads-test
|
||||
'frontend-tests.data.viewer-test
|
||||
'frontend-tests.data.workspace-colors-test
|
||||
'frontend-tests.data.workspace-comments-test
|
||||
'frontend-tests.data.workspace-interactions-test
|
||||
'frontend-tests.data.workspace-mcp-test
|
||||
'frontend-tests.data.workspace-media-test
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
||||
"packageManager": "pnpm@11.10.0+sha512.0b7f8b98060031904c017e3a41eb187a16d40eeb829b95c4f8cb03681761fc4ab53dd219115b9b447f4dce1a05a214764461e7d3703392a9f32f9511ce8c86c8",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
|
||||
@ -52,6 +52,8 @@
|
||||
- **plugins-runtime**: `penpot.openPage()` (and `Page.openPage()`) now resolves immediately when the target page is already active, instead of waiting forever for a page-initialization event that never fires.
|
||||
- **plugins-runtime**: `Shape.shadows`, `Shape.exports` and grid `rows`/`columns` now return live proxies, so writing a member on a returned shadow/export/track (e.g. `shape.shadows[0].blur = 7`) persists to the shape instead of mutating a detached snapshot that was silently discarded. The shadow `color` remains a plain snapshot (reconfigure it by assigning `shadow.color`).
|
||||
- **plugins-runtime**: Setting a variant component's `path` now renames the whole variant (its container and every main instance), like the `name` setter already did, instead of renaming only the component and leaving the file referentially inconsistent (which the backend rejected on save with a `variant-component-bad-name` error).
|
||||
- **plugins-runtime**: `Page.getSharedPluginDataKeys(namespace)` now works instead of always raising a namespace validation error: the implementation expected a spurious leading argument, so the caller's `namespace` was read as a missing second argument.
|
||||
- **plugins-runtime**: Storing plugin data on a connected (non-local) shared library is now consistently rejected with a `setPluginData-non-local-library` error on the `Library` object as well as its assets (colors, typographies, components). Previously the `Library` object accepted the write and applied it optimistically, but plugin data is not part of library synchronization and the change only persists when the caller can edit the library file — on a read-only shared library it failed silently and was lost on reload. Plugin data can only be stored on the file currently being edited.
|
||||
|
||||
## 1.4.2 (2026-01-21)
|
||||
|
||||
|
||||
@ -34,6 +34,24 @@ describe('Library', () => {
|
||||
expect(Array.isArray(ctx.penpot.library.connected)).toBe(true);
|
||||
});
|
||||
|
||||
// The Library object itself carries plugin data (it extends PluginData),
|
||||
// stored on the underlying file. Exercised on the local library; the
|
||||
// connected-library case (writing to a shared library's file/assets) shares
|
||||
// the same code path but can't be fixtured here (connecting a library hangs).
|
||||
test('local library stores plugin data', (ctx) => {
|
||||
const lib = ctx.penpot.library.local;
|
||||
lib.setPluginData('k', 'v');
|
||||
expect(lib.getPluginData('k')).toBe('v');
|
||||
expect(lib.getPluginDataKeys()).toContain('k');
|
||||
});
|
||||
|
||||
test('local library stores shared plugin data', (ctx) => {
|
||||
const lib = ctx.penpot.library.local;
|
||||
lib.setSharedPluginData('ns', 'k', 'v');
|
||||
expect(lib.getSharedPluginData('ns', 'k')).toBe('v');
|
||||
expect(lib.getSharedPluginDataKeys('ns')).toContain('k');
|
||||
});
|
||||
|
||||
test('library elements expose a libraryId', (ctx) => {
|
||||
const color = ctx.penpot.library.local.createColor();
|
||||
expect(typeof color.libraryId).toBe('string');
|
||||
@ -136,6 +154,13 @@ describe('Library', () => {
|
||||
expect(color.getPluginData('k')).toBe('v');
|
||||
expect(color.getPluginDataKeys()).toContain('k');
|
||||
});
|
||||
|
||||
test('color shared plugin data round-trips', (ctx) => {
|
||||
const color = ctx.penpot.library.local.createColor();
|
||||
color.setSharedPluginData('ns', 'k', 'v');
|
||||
expect(color.getSharedPluginData('ns', 'k')).toBe('v');
|
||||
expect(color.getSharedPluginDataKeys('ns')).toContain('k');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Typographies', () => {
|
||||
@ -170,6 +195,13 @@ describe('Library', () => {
|
||||
expect(typo.getPluginDataKeys()).toContain('k');
|
||||
});
|
||||
|
||||
test('typography shared plugin data round-trips', (ctx) => {
|
||||
const typo = ctx.penpot.library.local.createTypography();
|
||||
typo.setSharedPluginData('ns', 'k', 'v');
|
||||
expect(typo.getSharedPluginData('ns', 'k')).toBe('v');
|
||||
expect(typo.getSharedPluginDataKeys('ns')).toContain('k');
|
||||
});
|
||||
|
||||
test('typography fontFamily and fontId round-trip', (ctx) => {
|
||||
const typo = ctx.penpot.library.local.createTypography();
|
||||
expect(typeof typo.fontFamily).toBe('string');
|
||||
@ -256,6 +288,15 @@ describe('Library', () => {
|
||||
expect(comp.getPluginDataKeys()).toContain('k');
|
||||
});
|
||||
|
||||
test('component shared plugin data round-trips', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
const comp = ctx.penpot.library.local.createComponent([rect]);
|
||||
comp.setSharedPluginData('ns', 'k', 'v');
|
||||
expect(comp.getSharedPluginData('ns', 'k')).toBe('v');
|
||||
expect(comp.getSharedPluginDataKeys('ns')).toContain('k');
|
||||
});
|
||||
|
||||
test('component instance and mainInstance return shapes', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
|
||||
@ -26,6 +26,41 @@ describe('Plugin data', () => {
|
||||
if (file) {
|
||||
file.setPluginData('fileKey', 'fileValue');
|
||||
expect(file.getPluginData('fileKey')).toBe('fileValue');
|
||||
expect(file.getPluginDataKeys()).toContain('fileKey');
|
||||
}
|
||||
});
|
||||
|
||||
test('shared plugin data round-trips on the file', (ctx) => {
|
||||
const file = ctx.penpot.currentFile;
|
||||
expect(file).not.toBeNull();
|
||||
if (file) {
|
||||
file.setSharedPluginData('ns', 'fileShared', 'fileSharedValue');
|
||||
expect(file.getSharedPluginData('ns', 'fileShared')).toBe(
|
||||
'fileSharedValue',
|
||||
);
|
||||
expect(file.getSharedPluginDataKeys('ns')).toContain('fileShared');
|
||||
}
|
||||
});
|
||||
|
||||
test('plugin data round-trips on a page', (ctx) => {
|
||||
const page = ctx.penpot.currentPage;
|
||||
expect(page).not.toBeNull();
|
||||
if (page) {
|
||||
page.setPluginData('pageKey', 'pageValue');
|
||||
expect(page.getPluginData('pageKey')).toBe('pageValue');
|
||||
expect(page.getPluginDataKeys()).toContain('pageKey');
|
||||
}
|
||||
});
|
||||
|
||||
test('shared plugin data round-trips on a page', (ctx) => {
|
||||
const page = ctx.penpot.currentPage;
|
||||
expect(page).not.toBeNull();
|
||||
if (page) {
|
||||
page.setSharedPluginData('ns', 'pageShared', 'pageSharedValue');
|
||||
expect(page.getSharedPluginData('ns', 'pageShared')).toBe(
|
||||
'pageSharedValue',
|
||||
);
|
||||
expect(page.getSharedPluginDataKeys('ns')).toContain('pageShared');
|
||||
}
|
||||
});
|
||||
|
||||
@ -45,6 +80,47 @@ describe('Plugin data', () => {
|
||||
expect(rect.getPluginDataKeys()).toContain('');
|
||||
});
|
||||
|
||||
// Keys are opaque strings: no case normalization is applied. A camelCase key
|
||||
// and its kebab-case spelling are distinct entries that each round-trip
|
||||
// independently. Pins that behaviour so a future key-normalization regression
|
||||
// (reported as camelCase keys "behaving incorrectly") would be caught here.
|
||||
test('camelCase and kebab-case keys are distinct and both round-trip', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
rect.setPluginData('myKey', 'camel');
|
||||
rect.setPluginData('my-key', 'kebab');
|
||||
expect(rect.getPluginData('myKey')).toBe('camel');
|
||||
expect(rect.getPluginData('my-key')).toBe('kebab');
|
||||
const keys = rect.getPluginDataKeys();
|
||||
expect(keys).toContain('myKey');
|
||||
expect(keys).toContain('my-key');
|
||||
});
|
||||
|
||||
// Same guarantee at the file-data storage location (a distinct code path from
|
||||
// shape/page objects), covering a camelCase key on the file itself.
|
||||
test('a camelCase key round-trips on the file', (ctx) => {
|
||||
const file = ctx.penpot.currentFile;
|
||||
expect(file).not.toBeNull();
|
||||
if (file) {
|
||||
file.setPluginData('camelCaseFileKey', 'value');
|
||||
expect(file.getPluginData('camelCaseFileKey')).toBe('value');
|
||||
expect(file.getPluginDataKeys()).toContain('camelCaseFileKey');
|
||||
}
|
||||
});
|
||||
|
||||
// camelCase is likewise preserved in the shared namespace and the shared key.
|
||||
test('camelCase shared namespace and key round-trip', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
rect.setSharedPluginData('myNamespace', 'myKey', 'camel');
|
||||
rect.setSharedPluginData('myNamespace', 'my-key', 'kebab');
|
||||
expect(rect.getSharedPluginData('myNamespace', 'myKey')).toBe('camel');
|
||||
expect(rect.getSharedPluginData('myNamespace', 'my-key')).toBe('kebab');
|
||||
const keys = rect.getSharedPluginDataKeys('myNamespace');
|
||||
expect(keys).toContain('myKey');
|
||||
expect(keys).toContain('my-key');
|
||||
});
|
||||
|
||||
test('setPluginData with a non-string value throws', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
|
||||
74
tools/detect-target-branch
Executable file
74
tools/detect-target-branch
Executable file
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# detect-target-branch
|
||||
#
|
||||
# Determines the target branch for a PR of the current branch.
|
||||
#
|
||||
# Uses git name-rev to find the nearest ref (branch/tag) that is an ancestor of
|
||||
# HEAD, falling back to a graph walk if name-rev returns the current branch.
|
||||
#
|
||||
# Pure local — no remote, no SSH, no network. Works even when the current
|
||||
# branch shares its tip commit with the target (not yet diverged).
|
||||
#
|
||||
# Usage:
|
||||
# tools/detect-target-branch # print branch name
|
||||
# tools/detect-target-branch --verbose # print "branch~N"
|
||||
#
|
||||
# Exit status:
|
||||
# 0 — target found
|
||||
# 1 — no target found (orphan commit, no other branches)
|
||||
|
||||
verbose=false
|
||||
|
||||
for arg do
|
||||
case "$arg" in
|
||||
--verbose|-v) verbose=true ;;
|
||||
--name-only) ;;
|
||||
--help|-h)
|
||||
sed -n '/^#/,/^$/p' "$0" | sed 's/^# //; s/^#$//'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $arg" >&2
|
||||
echo "Usage: $(basename "$0") [--verbose|-v] [--name-only] [--help|-h]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
current=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Use name-rev to find the nearest ref — it has smart tie-breaking
|
||||
raw=$(git name-rev --name-only HEAD 2>/dev/null)
|
||||
target=${raw%%~[0-9]*} # strip ~N distance suffix
|
||||
|
||||
# If name-rev returned the current branch itself, walk backwards
|
||||
if [ "$target" = "$current" ] || [ -z "$target" ]; then
|
||||
target=$(git rev-list HEAD | while read commit; do
|
||||
branch=$(git branch --points-at="$commit" --format='%(refname:short)' 2>/dev/null \
|
||||
| grep -v "^$current$" \
|
||||
| head -1 || true)
|
||||
if [ -n "$branch" ]; then
|
||||
echo "$branch"
|
||||
break
|
||||
fi
|
||||
done || true)
|
||||
|
||||
if [ -z "$target" ]; then
|
||||
echo "error: unable to determine target branch" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$verbose" = true ]; then
|
||||
target_commit=$(git rev-parse "$target" 2>/dev/null || true)
|
||||
if [ -n "$target_commit" ]; then
|
||||
distance=$(git rev-list --count HEAD ^"$target_commit" 2>/dev/null || echo "0")
|
||||
echo "${target}~${distance}"
|
||||
else
|
||||
echo "${target}~?"
|
||||
fi
|
||||
else
|
||||
echo "$target"
|
||||
fi
|
||||
@ -128,18 +128,24 @@ function formatEvalMessages(messages) {
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
port: 6064,
|
||||
port: null,
|
||||
host: "127.0.0.1",
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
help: false,
|
||||
resetSession: false,
|
||||
lastError: false,
|
||||
backend: false,
|
||||
frontend: false,
|
||||
code: null,
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === "-p" || a === "--port") {
|
||||
if (a === "--backend") {
|
||||
args.backend = true;
|
||||
} else if (a === "--frontend") {
|
||||
args.frontend = true;
|
||||
} else if (a === "-p" || a === "--port") {
|
||||
const val = argv[++i];
|
||||
if (val === undefined) { console.error("Error: --port requires a value."); process.exit(1); }
|
||||
args.port = parseInt(val, 10);
|
||||
@ -166,6 +172,19 @@ function parseArgs(argv) {
|
||||
}
|
||||
}
|
||||
|
||||
if (args.backend && args.frontend) {
|
||||
console.error("Error: --backend and --frontend are mutually exclusive.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (args.backend) {
|
||||
args.port = 6064;
|
||||
} else if (args.frontend) {
|
||||
args.port = 3447;
|
||||
} else if (args.port === null) {
|
||||
args.port = 6064;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@ -177,7 +196,9 @@ Evaluate Clojure code via a running nREPL server. Session state (defs, in-ns)
|
||||
persists across invocations via a stored session ID.
|
||||
|
||||
Options:
|
||||
-p, --port PORT nREPL port (default: 6064)
|
||||
--backend Connect to backend nREPL on port 6064 (default)
|
||||
--frontend Connect to frontend nREPL on port 3447
|
||||
-p, --port PORT nREPL port (default: 6064; overridden by --backend/--frontend)
|
||||
-H, --host HOST nREPL host (default: 127.0.0.1)
|
||||
-t, --timeout MILLISECONDS Timeout in milliseconds (default: 120000)
|
||||
--reset-session Discard stored session and start fresh
|
||||
@ -187,6 +208,8 @@ Options:
|
||||
Examples:
|
||||
${bin} '(def x 42)'
|
||||
${bin} 'x'
|
||||
${bin} --backend '(+ 1 2 3)'
|
||||
${bin} --frontend '(js/alert "hi")'
|
||||
${bin} --reset-session '(def x 0)'
|
||||
${bin} --last-error
|
||||
${bin} <<'EOF'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user