diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e4021568ca..9fa432e7d3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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" diff --git a/.gitignore b/.gitignore index 224d199dc3..9958d90cb8 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ /notes /playground/ /backend/*.md +!/backend/AGENTS.md /backend/*.sql /backend/*.txt /backend/assets/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..d126301300 --- /dev/null +++ b/AGENTS.md @@ -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/`. + +```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: ` ` + +``` +:bug: Fix unexpected error on launching modal + +Optional body explaining the why. + +Signed-off-by: Fullname +``` + +**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 | diff --git a/backend/AGENTS.md b/backend/AGENTS.md new file mode 100644 index 0000000000..f0b4a7314c --- /dev/null +++ b/backend/AGENTS.md @@ -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/`. + +```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)`. diff --git a/backend/package.json b/backend/package.json index f3f4c18476..63bf06eddf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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/" } } diff --git a/common/package.json b/common/package.json index 9e1343ef20..09de4e95aa 100644 --- a/common/package.json +++ b/common/package.json @@ -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", diff --git a/exporter/package.json b/exporter/package.json index 9471814939..70b64bea7d 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -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/" } } diff --git a/frontend/package.json b/frontend/package.json index 31340bb1eb..dbccca7263 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/scripts/build-libs.js b/frontend/scripts/build-libs.js index b2bbe30559..a1aff27f2b 100644 --- a/frontend/scripts/build-libs.js +++ b/frontend/scripts/build-libs.js @@ -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 = diff --git a/library/package.json b/library/package.json index 46dc4fbac8..c3f3d1c32a 100644 --- a/library/package.json +++ b/library/package.json @@ -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" diff --git a/package.json b/package.json index 0a6d43e4f6..f38f80617d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "fmt": "./scripts/fmt" }, "devDependencies": { + "@github/copilot": "^1.0.2", "@types/node": "^20.12.7", "esbuild": "^0.25.9" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000..bec7b49e31 --- /dev/null +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/render-wasm/AGENTS.md b/render-wasm/AGENTS.md new file mode 100644 index 0000000000..378122985c --- /dev/null +++ b/render-wasm/AGENTS.md @@ -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. diff --git a/run-ci.sh b/run-ci.sh deleted file mode 100755 index a57d425924..0000000000 --- a/run-ci.sh +++ /dev/null @@ -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