📚 Add GitHub Copilot instructions (#8548)

This commit is contained in:
Andrey Antukh
2026-03-09 16:23:28 +01:00
committed by GitHub
parent adc3fa41e9
commit 05c71f7b75
14 changed files with 845 additions and 74 deletions

View File

@@ -28,9 +28,47 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check clojure code format
- name: Lint Common
working-directory: ./common
run: |
./scripts/lint
corepack enable;
corepack install;
pnpm install;
pnpm run lint:clj
- name: Lint Frontend
working-directory: ./frontend
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run lint:clj
pnpm run lint:js
pnpm run lint:scss
- name: Lint Backend
working-directory: ./backend
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run lint:clj
- name: Lint Exporter
working-directory: ./exporter
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run lint:clj
- name: Lint Library
working-directory: ./library
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run lint:clj
test-common:
name: "Common Tests"

1
.gitignore vendored
View File

@@ -35,6 +35,7 @@
/notes
/playground/
/backend/*.md
!/backend/AGENTS.md
/backend/*.sql
/backend/*.txt
/backend/assets/

265
AGENTS.md Normal file
View File

@@ -0,0 +1,265 @@
# Penpot Copilot Instructions
## Architecture Overview
Penpot is a full-stack design tool composed of several distinct components:
| Component | Language | Role |
|-----------|----------|------|
| `frontend/` | ClojureScript + SCSS | Single-page React app (design editor) |
| `backend/` | Clojure (JVM) | HTTP/RPC server, PostgreSQL, Redis |
| `common/` | Cljc (shared Clojure/ClojureScript) | Data types, geometry, schemas, utilities |
| `exporter/` | ClojureScript (Node.js) | Headless Playwright-based export (SVG/PDF) |
| `render-wasm/` | Rust → WebAssembly | High-performance canvas renderer using Skia |
| `mcp/` | TypeScript | Model Context Protocol integration |
| `plugins/` | TypeScript | Plugin runtime and example plugins |
The monorepo is managed with `pnpm` workspaces. The `manage.sh`
orchestrates cross-component builds. `run-ci.sh` defines the CI
pipeline.
---
## Build, Test & Lint Commands
### Frontend (`cd frontend`)
Run `./scripts/setup` for setup all dependencies.
```bash
# Dev
pnpm run watch:app # Full dev build (WASM + CLJS + assets)
# Production Build
./scripts/build
# Tests
pnpm run test # Build ClojureScript tests + run node target/tests/test.js
pnpm run watch:test # Watch + auto-rerun on change
pnpm run test:e2e # Playwright e2e tests
pnpm run test:e2e --grep "pattern" # Single e2e test by pattern
# Lint
pnpm run lint:js # format and linter check for JS
pnpm run lint:clj # format and linter check for CLJ
pnpm run lint:scss # prettier check for SCSS
# Code formatting
pnpm run fmt:clj # Format CLJ
pnpm run fmt:js # prettier for JS
pnpm run fmt:scss # prettier for SCSS
```
To run a focused ClojureScript unit test: edit
`test/frontend_tests/runner.cljs` to narrow the test suite, then `pnpm
run build:test && node target/tests/test.js`.
### Backend (`cd backend`)
```bash
# Tests (Kaocha)
clojure -M:dev:test # Full suite
clojure -M:dev:test --focus backend-tests.my-ns-test # Single namespace
# Lint / Format
pnpm run lint:clj
pnpm run fmt:clj
```
Test config is in `backend/tests.edn`; test namespaces match `.*-test$` under `test/`.
### Common (`cd common`)
```bash
pnpm run test # Build + run node target/tests/test.js
pnpm run watch:test # Watch mode
pnpm run lint:clj
pnpm run fmt:clj
```
### Render-WASM (`cd render-wasm`)
```bash
./test # Rust unit tests (cargo test)
./build # Compile to WASM (requires Emscripten)
cargo fmt --check
./lint --debug
```
## Key Conventions
### Namespace Structure
**Backend:**
- `app.rpc.commands.*` RPC command implementations (`auth`, `files`, `teams`, etc.)
- `app.http.*` HTTP routes and middleware
- `app.db.*` Database layer
- `app.tasks.*` Background job tasks
- `app.main` Integrant system setup and entrypoint
- `app.loggers` Internal loggers (auditlog, mattermost, etc) (do not be confused with `app.common.loggin`)
**Frontend:**
- `app.main.ui.*` React UI components (`workspace`, `dashboard`, `viewer`)
- `app.main.data.*` Potok event handlers (state mutations + side effects)
- `app.main.refs` Reactive subscriptions (okulary lenses)
- `app.main.store` Potok event store
- `app.util.*` Utilities (DOM, HTTP, i18n, keyboard shortcuts)
**Common:**
- `app.common.types.*` Shared data types for shapes, files, pages
- `app.common.schema` Malli validation schemas
- `app.common.geom.*` Geometry utilities
- `app.common.data.macros` Performance macros used everywhere
### Backend RPC Commands
All API calls go through a single RPC endpoint: `POST /api/rpc/command/<cmd-name>`.
```clojure
(sv/defmethod ::my-command
{::rpc/auth true ;; requires auth
::doc/added "1.18"
::sm/params [:map ...] ;; malli input schema
::sm/result [:map ...]} ;; malli output schema
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
;; return a plain map or throw
{:id (uuid/next)})
```
### Frontend State Management (Potok)
State is a single atom managed by a Potok store. Events implement protocols:
```clojure
(defn my-event [data]
(ptk/reify ::my-event
ptk/UpdateEvent
(update [_ state] ;; synchronous state transition
(assoc state :key data))
ptk/WatchEvent
(watch [_ state stream] ;; async: returns an observable
(->> (rp/cmd! :some-rpc-command params)
(rx/map success-event)
(rx/catch error-handler)))
ptk/EffectEvent
(effect [_ state _] ;; pure side effects (DOM, logging)
(.focus (dom/get-element "id")))))
```
Dispatch with `(st/emit! (my-event data))`. Read state via reactive
refs: `(deref refs/selected-shapes)`. Prefer helpers from
`app.util.dom` instead of using direct dom calls, if no helper is
available, prefer adding a new helper for handling it and the use the
new helper.
### CSS Modules Pattern
Styles are co-located with components. Each `.cljs` file has a corresponding `.scss` file:
```clojure
;; In the component namespace:
(require '[app.main.style :as stl])
;; In the render function:
[:div {:class (stl/css :container :active)}]
;; Conditional:
[:div {:class (stl/css-case :some-class true :selected (= drawtool :rect))}]
;; When you need concat an existing class:
[:div {:class [existing-class (stl/css-case :some-class true :selected (= drawtool :rect))]}]
```
### Performance Macros (`app.common.data.macros`)
Always prefer these macros over their `clojure.core` equivalents — they compile to faster JavaScript:
```clojure
(dm/select-keys m [:a :b]) ;; ~6x faster than core/select-keys
(dm/get-in obj [:a :b :c]) ;; faster than core/get-in
(dm/str "a" "b" "c") ;; string concatenation
```
### Shared Code (cljc)
Files in `common/src/app/common/` use reader conditionals to target both runtimes:
```clojure
#?(:clj (import java.util.UUID)
:cljs (:require [cljs.core :as core]))
```
Both frontend and backend depend on `common` as a local library (`penpot/common {:local/root "../common"}`).
### Component Definition (Rumext / React)
The codebase has several kind of components, some of them use legacy
syntax. The current and the most recent syntax uses `*` suffix on the
name. This indicates to the `mf/defc` macro apply concrete rules on
how props should be treated.
```clojure
(mf/defc my-component*
{::mf/wrap [mf/memo]} ;; React.memo
[{:keys [name on-click]}]
[:div {:class (stl/css :root)
:on-click on-click}
name])
```
Hooks: `(mf/use-state)`, `(mf/use-effect)`, `(mf/use-memo)` analgous to react hooks.
The component usage should always follow the `[:> my-component*
props]`, where props should be a map literal or symbol pointing to
javascript props objects. The javascript props object can be created
manually `#js {:data-foo "bar"}` or using `mf/spread-object` helper
macro.
---
## Commit Guidelines
Format: `<emoji-code> <subject>`
```
:bug: Fix unexpected error on launching modal
Optional body explaining the why.
Signed-off-by: Fullname <email>
```
**Subject rules:** imperative mood, capitalize first letter, no
trailing period, ≤ 80 characters. Add an entry to `CHANGES.md` if
applicable.
**Code patches must include a DCO sign-off** (`git commit -s`).
| Emoji | Emoji-Code | Use for |
|-------|------|---------|
| 🐛 | `:bug:` | Bug fix |
| ✨ | `:sparkles:` | Improvement |
| 🎉 | `:tada:` | New feature |
| ♻️ | `:recycle:` | Refactor |
| 💄 | `:lipstick:` | Cosmetic changes |
| 🚑 | `:ambulance:` | Critical bug fix |
| 📚 | `:books:` | Docs |
| 🚧 | `:construction:` | WIP |
| 💥 | `:boom:` | Breaking change |
| 🔧 | `:wrench:` | Config update |
| ⚡ | `:zap:` | Performance |
| 🐳 | `:whale:` | Docker |
| 📎 | `:paperclip:` | Other non-relevant changes |
| ⬆️ | `:arrow_up:` | Dependency upgrade |
| ⬇️ | `:arrow_down:` | Dependency downgrade |
| 🔥 | `:fire:` | Remove files or code |
| 🌐 | `:globe_with_meridians:` | Translations |

87
backend/AGENTS.md Normal file
View File

@@ -0,0 +1,87 @@
# backend Agent Instructions
Clojure service running on the JVM. Uses Integrant for dependency injection, PostgreSQL for storage, and Redis for messaging/caching.
## Commands
```bash
# REPL (primary dev workflow)
./scripts/repl # Start nREPL + load dev/user.clj utilities
# Tests (Kaocha)
clojure -M:dev:test # Full suite
clojure -M:dev:test --focus backend-tests.my-ns-test # Single namespace
# Lint / Format
pnpm run lint:clj
pnpm run fmt:clj
```
Test namespaces match `.*-test$` under `test/`. Config is in `tests.edn`.
## Integrant System
`src/app/main.clj` declares the system map. Each key is a component;
values are config maps with `::ig/ref` for dependencies. Components
implement `ig/init-key` / `ig/halt-key!`.
From the REPL (`dev/user.clj` is auto-loaded):
```clojure
(start!) ; boot the system
(stop!) ; halt the system
(restart!) ; stop + reload namespaces + start
```
## RPC Commands
All API calls: `POST /api/rpc/command/<cmd-name>`.
```clojure
(sv/defmethod ::my-command
{::rpc/auth true ;; requires authentication (default)
::doc/added "1.18"
::sm/params [:map ...] ;; malli input schema
::sm/result [:map ...]} ;; malli output schema
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
;; return a plain map; throw via ex/raise for errors
{:id (uuid/next)})
```
Add new commands in `src/app/rpc/commands/`.
## Database
`app.db` wraps next.jdbc. Queries use a SQL builder that auto-converts kebab-case ↔ snake_case.
```clojure
;; Query helpers
(db/get pool :table {:id id}) ; fetch one row (throws if missing)
(db/get* pool :table {:id id}) ; fetch one row (returns nil)
(db/query pool :table {:team-id team-id}) ; fetch multiple rows
(db/insert! pool :table {:name "x" :team-id id}) ; insert
(db/update! pool :table {:name "y"} {:id id}) ; update
(db/delete! pool :table {:id id}) ; delete
;; Transactions
(db/tx-run cfg (fn [{:keys [::db/conn]}]
(db/insert! conn :table row)))
```
Almost all methods on `app.db` namespace accepts `pool`, `conn` or
`cfg` as params.
Migrations live in `src/app/migrations/` as numbered SQL files. They run automatically on startup.
## Error Handling
```clojure
(ex/raise :type :not-found
:code :object-not-found
:hint "File does not exist"
:context {:id file-id})
```
Common types: `:not-found`, `:validation`, `:authorization`, `:conflict`, `:internal`.
## Configuration
`src/app/config.clj` reads `PENPOT_*` environment variables, validated with Malli. Access anywhere via `(cf/get :smtp-host)`. Feature flags: `(cf/flags :enable-smtp)`.

View File

@@ -19,8 +19,7 @@
"ws": "^8.17.0"
},
"scripts": {
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"lint:clj": "clj-kondo --parallel --lint src/"
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel --lint src/",
"fmt:clj": "cljfmt fix --parallel=true src/ test/"
}
}

View File

@@ -20,9 +20,8 @@
"date-fns": "^4.1.0"
},
"scripts": {
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel=true --lint src/",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"lint:clj": "clj-kondo --parallel=true --lint src/",
"lint": "pnpm run lint:clj",
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests/ --exec 'node target/tests/test.js'\"",
"build:test": "clojure -M:dev:shadow-cljs compile test",

View File

@@ -34,8 +34,7 @@
"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:clj:check": "cljfmt check --parallel=false src/",
"fmt:clj": "cljfmt fix --parallel=true src/",
"lint:clj": "clj-kondo --parallel --lint src/"
"lint:clj": "cljfmt check --parallel src/ && clj-kondo --parallel --lint src/"
}
}

View File

@@ -24,12 +24,11 @@
"build:app:worker": "clojure -M:dev:shadow-cljs release worker",
"build:app": "pnpm run clear:shadow-cache && pnpm run build:app:main && pnpm run build:app:libs",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
"fmt:js": "pnpx prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
"fmt:js:check": "pnpx prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js text-editor/**/*.js",
"lint:clj": "clj-kondo --parallel --lint src/",
"lint:scss": "pnpx prettier -c resources/styles -c src/**/*.scss",
"lint:scss:fix": "pnpx prettier -c resources/styles -c src/**/*.scss -w",
"fmt:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
"fmt:scss": "prettier -c resources/styles -c src/**/*.scss -w",
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel --lint src/",
"lint:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js text-editor/**/*.js",
"lint:scss": "prettier -c resources/styles -c src/**/*.scss",
"build:test": "clojure -M:dev:shadow-cljs compile test",
"test": "pnpm run build:test && node target/tests/test.js",
"test:storybook": "vitest run --project=storybook",

View File

@@ -5,14 +5,17 @@ import { readFile } from "node:fs/promises";
* esbuild plugin to watch a directory recursively
*/
const watchExtraDirPlugin = {
name: 'watch-extra-dir',
name: "watch-extra-dir",
setup(build) {
build.onLoad({ filter: /target\/index.js/, namespace: 'file' }, async (args) => {
return {
watchDirs: ["packages/ui/dist"],
};
});
}
build.onLoad(
{ filter: /target\/index.js/, namespace: "file" },
async (args) => {
return {
watchDirs: ["packages/ui/dist"],
};
},
);
},
};
const filter =

View File

@@ -27,8 +27,7 @@
"build": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs release library",
"build:bundle": "./scripts/build",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
"lint:clj": "clj-kondo --parallel --lint src/",
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel --lint src/",
"test": "node --test",
"watch:test": "node --test --watch",
"watch": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library"

View File

@@ -15,6 +15,7 @@
"fmt": "./scripts/fmt"
},
"devDependencies": {
"@github/copilot": "^1.0.2",
"@types/node": "^20.12.7",
"esbuild": "^0.25.9"
}

370
pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,370 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@github/copilot':
specifier: ^1.0.2
version: 1.0.2
'@types/node':
specifier: ^20.12.7
version: 20.19.37
esbuild:
specifier: ^0.25.9
version: 0.25.12
packages:
'@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.12':
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.12':
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.12':
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.12':
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.12':
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.12':
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.12':
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.12':
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.12':
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.12':
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.12':
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.12':
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.12':
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.12':
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.12':
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.12':
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.12':
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.12':
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.12':
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.12':
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.12':
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.12':
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.12':
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.12':
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.12':
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@github/copilot-darwin-arm64@1.0.2':
resolution: {integrity: sha512-dYoeaTidsphRXyMjvAgpjEbBV41ipICnXURrLFEiATcjC4IY6x2BqPOocrExBYW/Tz2VZvDw51iIZaf6GXrTmw==}
cpu: [arm64]
os: [darwin]
hasBin: true
'@github/copilot-darwin-x64@1.0.2':
resolution: {integrity: sha512-8+Z9dYigEfXf0wHl9c2tgFn8Cr6v4RAY8xTgHMI9mZInjQyxVeBXCxbE2VgzUtDUD3a705Ka2d8ZOz05aYtGsg==}
cpu: [x64]
os: [darwin]
hasBin: true
'@github/copilot-linux-arm64@1.0.2':
resolution: {integrity: sha512-ik0Y5aTXOFRPLFrNjZJdtfzkozYqYeJjVXGBAH3Pp1nFZRu/pxJnrnQ1HrqO/LEgQVbJzAjQmWEfMbXdQIxE4Q==}
cpu: [arm64]
os: [linux]
hasBin: true
'@github/copilot-linux-x64@1.0.2':
resolution: {integrity: sha512-mHSPZjH4nU9rwbfwLxYJ7CQ90jK/Qu1v2CmvBCUPfmuGdVwrpGPHB5FrB+f+b0NEXjmemDWstk2zG53F7ppHfw==}
cpu: [x64]
os: [linux]
hasBin: true
'@github/copilot-win32-arm64@1.0.2':
resolution: {integrity: sha512-tLW2CY/vg0fYLp8EuiFhWIHBVzbFCDDpohxT/F/XyMAdTVSZLnopCcxQHv2BOu0CVGrYjlf7YOIwPfAKYml1FA==}
cpu: [arm64]
os: [win32]
hasBin: true
'@github/copilot-win32-x64@1.0.2':
resolution: {integrity: sha512-cFlc3xMkKKFRIYR00EEJ2XlYAemeh5EZHsGA8Ir2G0AH+DOevJbomdP1yyCC5gaK/7IyPkHX3sGie5sER2yPvQ==}
cpu: [x64]
os: [win32]
hasBin: true
'@github/copilot@1.0.2':
resolution: {integrity: sha512-716SIZMYftldVcJay2uZOzsa9ROGGb2Mh2HnxbDxoisFsWNNgZlQXlV7A+PYoGsnAo2Zk/8e1i5SPTscGf2oww==}
hasBin: true
'@types/node@20.19.37':
resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==}
esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
snapshots:
'@esbuild/aix-ppc64@0.25.12':
optional: true
'@esbuild/android-arm64@0.25.12':
optional: true
'@esbuild/android-arm@0.25.12':
optional: true
'@esbuild/android-x64@0.25.12':
optional: true
'@esbuild/darwin-arm64@0.25.12':
optional: true
'@esbuild/darwin-x64@0.25.12':
optional: true
'@esbuild/freebsd-arm64@0.25.12':
optional: true
'@esbuild/freebsd-x64@0.25.12':
optional: true
'@esbuild/linux-arm64@0.25.12':
optional: true
'@esbuild/linux-arm@0.25.12':
optional: true
'@esbuild/linux-ia32@0.25.12':
optional: true
'@esbuild/linux-loong64@0.25.12':
optional: true
'@esbuild/linux-mips64el@0.25.12':
optional: true
'@esbuild/linux-ppc64@0.25.12':
optional: true
'@esbuild/linux-riscv64@0.25.12':
optional: true
'@esbuild/linux-s390x@0.25.12':
optional: true
'@esbuild/linux-x64@0.25.12':
optional: true
'@esbuild/netbsd-arm64@0.25.12':
optional: true
'@esbuild/netbsd-x64@0.25.12':
optional: true
'@esbuild/openbsd-arm64@0.25.12':
optional: true
'@esbuild/openbsd-x64@0.25.12':
optional: true
'@esbuild/openharmony-arm64@0.25.12':
optional: true
'@esbuild/sunos-x64@0.25.12':
optional: true
'@esbuild/win32-arm64@0.25.12':
optional: true
'@esbuild/win32-ia32@0.25.12':
optional: true
'@esbuild/win32-x64@0.25.12':
optional: true
'@github/copilot-darwin-arm64@1.0.2':
optional: true
'@github/copilot-darwin-x64@1.0.2':
optional: true
'@github/copilot-linux-arm64@1.0.2':
optional: true
'@github/copilot-linux-x64@1.0.2':
optional: true
'@github/copilot-win32-arm64@1.0.2':
optional: true
'@github/copilot-win32-x64@1.0.2':
optional: true
'@github/copilot@1.0.2':
optionalDependencies:
'@github/copilot-darwin-arm64': 1.0.2
'@github/copilot-darwin-x64': 1.0.2
'@github/copilot-linux-arm64': 1.0.2
'@github/copilot-linux-x64': 1.0.2
'@github/copilot-win32-arm64': 1.0.2
'@github/copilot-win32-x64': 1.0.2
'@types/node@20.19.37':
dependencies:
undici-types: 6.21.0
esbuild@0.25.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12
'@esbuild/android-arm': 0.25.12
'@esbuild/android-arm64': 0.25.12
'@esbuild/android-x64': 0.25.12
'@esbuild/darwin-arm64': 0.25.12
'@esbuild/darwin-x64': 0.25.12
'@esbuild/freebsd-arm64': 0.25.12
'@esbuild/freebsd-x64': 0.25.12
'@esbuild/linux-arm': 0.25.12
'@esbuild/linux-arm64': 0.25.12
'@esbuild/linux-ia32': 0.25.12
'@esbuild/linux-loong64': 0.25.12
'@esbuild/linux-mips64el': 0.25.12
'@esbuild/linux-ppc64': 0.25.12
'@esbuild/linux-riscv64': 0.25.12
'@esbuild/linux-s390x': 0.25.12
'@esbuild/linux-x64': 0.25.12
'@esbuild/netbsd-arm64': 0.25.12
'@esbuild/netbsd-x64': 0.25.12
'@esbuild/openbsd-arm64': 0.25.12
'@esbuild/openbsd-x64': 0.25.12
'@esbuild/openharmony-arm64': 0.25.12
'@esbuild/sunos-x64': 0.25.12
'@esbuild/win32-arm64': 0.25.12
'@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12
undici-types@6.21.0: {}

61
render-wasm/AGENTS.md Normal file
View File

@@ -0,0 +1,61 @@
# render-wasm Agent Instructions
This component compiles Rust to WebAssembly using Emscripten + Skia. It is consumed by the frontend as a canvas renderer.
## Commands
```bash
./build # Compile Rust → WASM (requires Emscripten environment)
./watch # Incremental rebuild on file change
./test # Run Rust unit tests (cargo test)
./lint # clippy -D warnings
cargo fmt --check
```
Run a single test:
```bash
cargo test my_test_name # by test function name
cargo test shapes:: # by module prefix
```
Build output lands in `../frontend/resources/public/js/` (consumed directly by the frontend dev server).
## Build Environment
The `_build_env` script sets required env vars (Emscripten paths,
`EMCC_CFLAGS`). `./build` sources it automatically. The WASM heap is
configured to 256 MB initial with geometric growth.
## Architecture
**Global state** — a single `unsafe static mut State` accessed
exclusively through `with_state!` / `with_state_mut!` macros. Never
access it directly.
**Tile-based rendering** — only 512×512 tiles within the viewport
(plus a pre-render buffer) are drawn each frame. Tiles outside the
range are skipped.
**Two-phase updates** — shape data is written via exported setter
functions (called from ClojureScript), then a single `render_frame()`
triggers the actual Skia draw calls.
**Shape hierarchy** — shapes live in a flat pool indexed by UUID;
parent/child relationships are tracked separately.
## Key Source Modules
| Path | Role |
|------|------|
| `src/lib.rs` | WASM exports — all functions callable from JS |
| `src/state.rs` | Global `State` struct definition |
| `src/render/` | Tile rendering pipeline, Skia surface management |
| `src/shapes/` | Shape types and Skia draw logic per shape |
| `src/wasm/` | JS interop helpers (memory, string encoding) |
## Frontend Integration
The WASM module is loaded by `app.render-wasm.*` namespaces in the
frontend. ClojureScript calls exported Rust functions to push shape
data, then calls `render_frame`. Do not change export function
signatures without updating the ClojureScript bridge.

View File

@@ -1,50 +0,0 @@
#!/bin/bash
set -e
echo "################ test common ################"
pushd common
pnpm install
pnpm run fmt:clj:check
pnpm run lint:clj
clojure -M:dev:test
pnpm run test
popd
echo "################ test frontend ################"
pushd frontend
pnpm install
pnpm run fmt:clj:check
pnpm run fmt:js:check
pnpm run lint:scss
pnpm run lint:clj
pnpm run test
popd
echo "################ test integration ################"
pushd frontend
pnpm install
pnpm run test:e2e -x --workers=4
popd
echo "################ test backend ################"
pushd backend
pnpm install
pnpm run fmt:clj:check
pnpm run lint:clj
clojure -M:dev:test --reporter kaocha.report/documentation
popd
echo "################ test exporter ################"
pushd exporter
pnpm install
pnpm run fmt:clj:check
pnpm run lint:clj
popd
echo "################ test render-wasm ################"
pushd render-wasm
cargo fmt --check
./lint --debug
./test
popd