diff --git a/backend/AGENTS.md b/backend/AGENTS.md deleted file mode 100644 index a260a74c13..0000000000 --- a/backend/AGENTS.md +++ /dev/null @@ -1,262 +0,0 @@ -# Penpot Backend – Agent Instructions - -Clojure backend (RPC) service running on the JVM. - -Uses Integrant for dependency injection, PostgreSQL for storage, and -Redis for messaging/caching. - -## General Guidelines - -To ensure consistency across the Penpot JVM stack, all contributions must adhere -to these criteria. - -IMPORTANT: all CLI commands should be executed under backend/ -subdirectory for make them work correctly. - -### 1. Testing & Validation - -* **Coverage:** If code is added or modified in `src/`, corresponding - tests in `test/backend_tests/` must be added or updated. - -* **Execution:** - * **Isolated:** Run `clojure -M:dev:test --focus backend-tests.my-ns-test` for the specific test namespace. - * **Regression:** Run `clojure -M:dev:test` to ensure the suite passes without regressions in related functional areas. - -### 2. Code Quality & Formatting - -* **Linting:** All code must pass linter checks (run `pnpm run lint:clj` or `pnpm run lint` on the repository root) -* **Formatting:** All the code must pass the formatting check (run `pnpm run - check-fmt`). Use `pnpm run fmt` to fix formatting issues. Avoid "dirty" - diffs caused by unrelated whitespace changes. -* **Type Hinting:** Use explicit JVM type hints (e.g., `^String`, `^long`) in - performance-critical paths to avoid reflection overhead. - -## Code Conventions - -### Namespace Overview - -The source is located under `src` directory and this is a general overview of -namespaces structure: - -- `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.) (not to be confused with `app.common.logging`) - -### RPC - -The RPC methods are implemented using a multimethod-like structure via the -`app.util.services` namespace. The main RPC methods are collected under -`app.rpc.commands` namespace and exposed under `/api/rpc/command/`. - -The RPC method accepts POST and GET requests indistinctly and uses the `Accept` -header to negotiate the response encoding (which can be Transit — the default — -or plain JSON). It also accepts Transit (default) or JSON as input, which should -be indicated using the `Content-Type` header. - -The main convention is: use `get-` prefix on RPC name when we want READ -operation. - -Example of RPC method definition: - -```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)}) -``` - -Look under `src/app/rpc/commands/*.clj` to see more examples. - -### Tests - -Test namespaces match `.*-test$` under `test/`. Config is in `tests.edn`. - - -### Integrant System - -The `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!`. - - -### Connecting to the Database - -Two PostgreSQL databases are used in this environment: - -| Database | Purpose | Connection string | -|---------------|--------------------|----------------------------------------------------| -| `penpot` | Development / app | `postgresql://penpot:penpot@postgres/penpot` | -| `penpot_test` | Test suite | `postgresql://penpot:penpot@postgres/penpot_test` | - -**Interactive psql session:** - -```bash -# development DB -psql "postgresql://penpot:penpot@postgres/penpot" - -# test DB -psql "postgresql://penpot:penpot@postgres/penpot_test" -``` - -**One-shot query (non-interactive):** - -```bash -psql "postgresql://penpot:penpot@postgres/penpot" -c "SELECT id, name FROM team LIMIT 5;" -``` - -**Useful psql meta-commands:** - -``` -\dt -- list all tables -\d -- describe a table (columns, types, constraints) -\di -- list indexes -\q -- quit -``` - -> **Migrations table:** Applied migrations are tracked in the `migrations` table -> with columns `module`, `step`, and `created_at`. When renaming a migration -> logical name, update this table in both databases to match the new name; -> otherwise the runner will attempt to re-apply the migration on next startup. - -```bash -# Example: fix a renamed migration entry in the test DB -psql "postgresql://penpot:penpot@postgres/penpot_test" \ - -c "UPDATE migrations SET step = 'new-name' WHERE step = 'old-name';" -``` - -### Database Access (Clojure) - -`app.db` wraps next.jdbc. Queries use a SQL builder that auto-converts kebab-case ↔ snake_case. - -```clojure -;; Query helpers -(db/get cfg-or-pool :table {:id id}) ; fetch one row (throws if missing) -(db/get* cfg-or-pool :table {:id id}) ; fetch one row (returns nil) -(db/query cfg-or-pool :table {:team-id team-id}) ; fetch multiple rows -(db/insert! cfg-or-pool :table {:name "x" :team-id id}) ; insert -(db/update! cfg-or-pool :table {:name "y"} {:id id}) ; update -(db/delete! cfg-or-pool :table {:id id}) ; delete - -;; Run multiple statements/queries on single connection -(db/run! cfg (fn [{:keys [::db/conn]}] - (db/insert! conn :table row1) - (db/insert! conn :table row2)) - - -;; Transactions -(db/tx-run! cfg (fn [{:keys [::db/conn]}] - (db/insert! conn :table row))) -``` - -Almost all methods in the `app.db` namespace accept `pool`, `conn`, or -`cfg` as params. - -Migrations live in `src/app/migrations/` as numbered SQL files. They run automatically on startup. - - -### Error Handling - -The exception helpers are defined on Common module, and are available under -`app.common.exceptions` namespace. - -Example of raising an exception: - -```clojure -(ex/raise :type :not-found - :code :object-not-found - :hint "File does not exist" - :file-id id) -``` - -Common types: `:not-found`, `:validation`, `:authorization`, `:conflict`, `:internal`. - - -### Performance Macros (`app.common.data.macros`) - -Always prefer these macros over their `clojure.core` equivalents — they provide -optimized implementations: - -```clojure -(dm/select-keys m [:a :b]) ;; faster than core/select-keys -(dm/get-in obj [:a :b :c]) ;; faster than core/get-in -(dm/str "a" "b" "c") ;; string concatenation -``` - -### 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)`. - - -### Background Tasks - -Background tasks live in `src/app/tasks/`. Each task is an Integrant component -that exposes a `::handler` key and follows this three-method pattern: - -```clojure -(defmethod ig/assert-key ::handler ;; validate config at startup - [_ params] - (assert (db/pool? (::db/pool params)) "expected a valid database pool")) - -(defmethod ig/expand-key ::handler ;; inject defaults before init - [k v] - {k (assoc v ::my-option default-value)}) - -(defmethod ig/init-key ::handler ;; return the task fn - [_ cfg] - (fn [_task] ;; receives the task row from the worker - (db/tx-run! cfg (fn [{:keys [::db/conn]}] - ;; … do work … - )))) -``` - -**Wiring a new task** requires two changes in `src/app/main.clj`: - -1. **Handler config** – add an entry in `system-config` with the dependencies: - -```clojure -:app.tasks.my-task/handler -{::db/pool (ig/ref ::db/pool)} -``` - -2. **Registry + cron** – register the handler name and schedule it: - -```clojure -;; in ::wrk/registry ::wrk/tasks map: -:my-task (ig/ref :app.tasks.my-task/handler) - -;; in worker-config ::wrk/cron ::wrk/entries vector: -{:cron #penpot/cron "0 0 0 * * ?" ;; daily at midnight - :task :my-task} -``` - -**Useful cron patterns** (Quartz format — six fields: s m h dom mon dow): - -| Expression | Meaning | -|------------------------------|--------------------| -| `"0 0 0 * * ?"` | Daily at midnight | -| `"0 0 */6 * * ?"` | Every 6 hours | -| `"0 */5 * * * ?"` | Every 5 minutes | - -**Time helpers** (`app.common.time`): - -```clojure -(ct/now) ;; current instant -(ct/duration {:hours 1}) ;; java.time.Duration -(ct/minus (ct/now) some-duration) ;; subtract duration from instant -``` - -`db/interval` converts a `Duration` (or millis / string) to a PostgreSQL -interval object suitable for use in SQL queries: - -```clojure -(db/interval (ct/duration {:hours 1})) ;; → PGInterval "3600.0 seconds" -``` diff --git a/common/AGENTS.md b/common/AGENTS.md deleted file mode 100644 index 2659b83939..0000000000 --- a/common/AGENTS.md +++ /dev/null @@ -1,70 +0,0 @@ -# Penpot Common – Agent Instructions - -A shared module with code written in Clojure, ClojureScript, and -JavaScript. Contains multiplatform code that can be used and executed -from the frontend, backend, or exporter modules. It uses Clojure reader -conditionals to specify platform-specific implementations. - -## General Guidelines - -To ensure consistency across the Penpot stack, all contributions must adhere to -these criteria: - -### 1. Testing & Validation - -If code is added or modified in `src/`, corresponding tests in -`test/common_tests/` must be added or updated. - - * **Environment:** Tests should run in both JS (Node.js) and JVM environments. -* **Location:** Place tests in the `test/common_tests/` directory, following the - namespace structure of the source code (e.g., `app.common.colors` -> - `common-tests.colors-test`). -* **Execution:** Tests should be executed on both JS (Node.js) and JVM environments: - * **Isolated:** - * JS: To run a focused ClojureScript unit test: edit the - `test/common_tests/runner.cljs` to narrow the test suite, then - `pnpm run test:js`. - * JVM: `pnpm run test:jvm --focus common-tests.my-ns-test` - * **Regression:** - * JS: Run `pnpm run test:js` without modifications on the runner (preferred) - * JVM: Run `pnpm run test:jvm` - -### 2. Code Quality & Formatting - -* **Linting:** All code changes must pass linter checks: - * Run `pnpm run lint:clj` for CLJ/CLJS/CLJC -* **Formatting:** All code changes must pass the formatting check - * Run `pnpm run check-fmt:clj` for CLJ/CLJS/CLJC - * Run `pnpm run check-fmt:js` for JS - * Use `pnpm run fmt` to fix all formatting issues (`pnpm run - fmt:clj` or `pnpm run fmt:js` for isolated formatting fix). - -## Code Conventions - -### Namespace Overview - -The source is located under `src` directory and this is a general overview of -namespaces structure: - -- `app.common.types.*` – Shared data types for shapes, files, pages using Malli schemas -- `app.common.schema` – Malli abstraction layer, exposes the most used functions from malli -- `app.common.geom.*` – Geometry and shape transformation helpers -- `app.common.data` – Generic helpers used across the entire application -- `app.common.math` – Generic math helpers used across the entire application -- `app.common.json` – Generic JSON encoding/decoding helpers -- `app.common.data.macros` – Performance macros used everywhere - - -### Reader Conditionals - -We use reader conditionals to differentiate implementations depending on the -target platform where the code runs: - -```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"}`). - diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md deleted file mode 100644 index 681528b4f3..0000000000 --- a/frontend/AGENTS.md +++ /dev/null @@ -1,371 +0,0 @@ -# Penpot Frontend – Agent Instructions - -ClojureScript-based frontend application that uses React and RxJS as its main -architectural pieces. - -## General Guidelines - -### 1. Testing & Validation - -#### Unit Tests - -If code is added or modified in `src/`, corresponding tests in -`test/frontend_tests/` must be added or updated. - -* **Environment:** Tests should run in a Node.js or browser-isolated - environment without requiring the full application state or a - running backend. Test are developed using cljs.test. -* **Mocks & Stubs:** * Use proper mocks for any side-effecting - functions (e.g., API calls, storage access). - * Avoid testing through the UI (DOM); we have e2e tests for that. - * Use `with-redefs` or similar ClojureScript mocking utilities to isolate the logic under test. -* **No Flakiness:** Tests must be deterministic. Do not use `setTimeout` or real - network calls. Use synchronous mocks for asynchronous workflows where - possible. -* **Location:** Place tests in the `test/frontend_tests/` directory, following the - namespace structure of the source code (e.g., `app.utils.timers` -> - `frontend-tests.util-timers-test`). -* **Execution:** - * **Isolated:** To run a focused ClojureScript unit test: edit the - `test/frontend_tests/runner.cljs` to narrow the test suite, then `pnpm run - test`. - * **Regression:** To run `pnpm run test` without modifications on the runner (preferred) - - -#### Integration Tests (Playwright) - -Integration tests are developed under `frontend/playwright` directory, we use -mocks for remote communication with the backend. - -You should not add, modify or run the integration tests unless explicitly asked. - - -``` -pnpm run test:e2e # Playwright e2e tests -pnpm run test:e2e --grep "pattern" # Single e2e test by pattern -``` - -Ensure everything is installed before executing tests with the `./scripts/setup` script. - - -### 2. Code Quality & Formatting - -* **Linting:** All code changes must pass linter checks: - * Run `pnpm run lint:clj` for CLJ/CLJS/CLJC - * Run `pnpm run lint:js` for JS - * Run `pnpm run lint:scss` for SCSS -* **Formatting:** All code changes must pass the formatting check - * Run `pnpm run check-fmt:clj` for CLJ/CLJS/CLJC - * Run `pnpm run check-fmt:js` for JS - * Run `pnpm run check-fmt:scss` for SCSS - * Use the `pnpm run fmt` fix all the formatting issues (`pnpm run fmt:clj`, - `pnpm run fmt:js` or `pnpm run fmt:scss` for isolated formatting fix) - -### 3. Implementation Rules - -* **Logic vs. View:** If logic is embedded in a UI component, extract it into a - function in the same namespace if it is only used locally, or look for a helper - namespace to make it unit-testable. - -### 4. Stack Trace Analysis - -When analyzing production stack traces (minified code), you can generate a -production bundle locally to map the minified code back to the source. - -**To build the production bundle:** - -Run: `pnpm run build:app` - -The compiled files and their corresponding source maps will be generated in -`resources/public/js`. - -**Analysis Tips:** - -- **Source Maps:** Use the `.map` files generated in `resources/public/js` with - tools like `source-map-lookup` or browser dev tools to resolve minified - locations. -- **Bundle Inspection:** If the issue is related to bundle size or unexpected - code inclusion, inspect the generated modules in `resources/public/js`. -- **Shadow-CLJS Reports:** For more detailed analysis of what is included in the - bundle, you can run shadow-cljs build reports (consult `shadow-cljs.edn` for - build IDs like `main` or `worker`). - - -## Code Conventions - -### Namespace Overview - -The source is located under `src` directory and this is a general overview of -namespaces structure: - -- `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) - - -### State Management (Potok) - -State is a single atom managed by a Potok store. Events implement protocols -(funcool/potok library): - -```clojure -(defn my-event - "doc string" - [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) - (dom/focus (dom/get-element "id"))))) -``` - -The state is located under `app.main.store` namespace where we have -the `emit!` function responsible for emitting events. - -Example: - -```cljs -(ns some.ns - (:require - [app.main.data.my-events :refer [my-event]] - [app.main.store :as st])) - -(defn on-click - [event] - (st/emit! (my-event))) -``` - -On `app.main.refs` we have reactive references which look up the main state -for inner data or precalculated data. These references are very useful but -should be used with care because, for example, if we have a complex operation, -this operation will be executed on each state change. Sometimes it is better to -have simple references and use React `use-memo` for more granular memoization. - -Prefer helpers from `app.util.dom` instead of using direct DOM calls. If no -helper is available, prefer adding a new helper and then using it. - -### UI Components (React & Rumext: mf/defc) - -The codebase contains various component patterns. When creating or refactoring -components, follow the Modern Syntax rules outlined below. - -#### 1. The * Suffix Convention - -The most recent syntax uses a * suffix in the component name (e.g., -my-component*). This suffix signals the mf/defc macro to apply specific rules -for props handling and destructuring and optimization. - -#### 2. Component Definition - -Modern components should use the following structure: - -```clj -(mf/defc my-component* - {::mf/wrap [mf/memo]} ;; Equivalent to React.memo - [{:keys [name on-click]}] ;; Destructured props - [:div {:class (stl/css :root) - :on-click on-click} - name]) -``` - -#### 3. Hooks - -Use the mf namespace for hooks to maintain consistency with the macro's -lifecycle management. These are analogous to standard React hooks: - -```clj -(mf/use-state) ;; analogous to React.useState adapted to cljs semantics -(mf/use-effect) ;; analogous to React.useEffect -(mf/use-memo) ;; analogous to React.useMemo -(mf/use-fn) ;; analogous to React.useCallback -``` - -The `mf/use-state` in difference with React.useState, returns an atom-like -object, where you can use `swap!` or `reset!` to perform an update and -`deref` to get the current value. - -You also have the `mf/deref` hook (which does not follow the `use-` naming -pattern) and its purpose is to watch (subscribe to changes on) an atom or -derived atom (from okulary) and get the current value. It is mainly used to -subscribe to lenses defined in `app.main.refs` or private lenses defined in -namespaces. - -Rumext also comes with improved syntax macros as alternative to `mf/use-effect` -and `mf/use-memo` functions. Examples: - - -Example for `mf/with-effect` macro: - -```clj -;; Using functions -(mf/use-effect - (mf/deps team-id) - (fn [] - (st/emit! (dd/initialize team-id)) - (fn [] - (st/emit! (dd/finalize team-id))))) - -;; The same effect but using mf/with-effect -(mf/with-effect [team-id] - (st/emit! (dd/initialize team-id)) - (fn [] - (st/emit! (dd/finalize team-id)))) -``` - -Example for `mf/with-memo` macro: - -``` -;; Using functions -(mf/use-memo - (mf/deps projects team-id) - (fn [] - (->> (vals projects) - (filterv #(= team-id (:team-id %)))))) - -;; Using the macro -(mf/with-memo [projects team-id] - (->> (vals projects) - (filterv #(= team-id (:team-id %))))) -``` - -Prefer using the macros for their syntax simplicity. - - -#### 4. Component Usage (Hiccup Syntax) - -When invoking a component within Hiccup, always use the [:> component* props] -pattern. - -Requirements for props: - -- Must be a map literal or a symbol pointing to a JavaScript props object. -- To create a JS props object, use the `#js` literal or the `mf/spread-object` helper macro. - -Examples: - -```clj -;; Using object literal (no need of #js because macro already interprets it) -[:> my-component* {:data-foo "bar"}] - -;; Using object literal (no need of #js because macro already interprets it) -(let [props #js {:data-foo "bar" - :className "myclass"}] - [:> my-component* props]) - -;; Using the spread helper -(let [props (mf/spread-object base-props {:extra "data"})] - [:> my-component* props]) -``` - -#### 5. Styles - -##### Styles on component code -Styles are co-located with components. Each `.cljs` file has a corresponding -`.scss` file. - -Example of clojurescript code for reference classes defined on styles (we use -CSS modules pattern): - -```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))]}] -``` - -##### General rules for styling - -- Prefer CSS custom properties ( `margin: var(--sp-xs);`) instead of scss - variables and get the already defined properties from `_sizes.scss`. The SCSS - variables are allowed and still used, just prefer properties if they are - already defined. -- If a value isn't in the DS, use the `px2rem(n)` mixin: `@use "ds/_utils.scss" - as *; padding: px2rem(23);`. -- Do **not** create new SCSS variables for one-off values. -- Use physical directions with logical ones to support RTL/LTR naturally: - - Avoid: `margin-left`, `padding-right`, `left`, `right`. - - Prefer: `margin-inline-start`, `padding-inline-end`, `inset-inline-start`. -- Always use the `use-typography` mixin from `ds/typography.scss`: - - Example: `@include t.use-typography("title-small");` -- Use `$br-*` for radius and `$b-*` for thickness from `ds/_borders.scss`. -- Use only tokens from `ds/colors.scss`. Do **NOT** use `design-tokens.scss` or - legacy color variables. -- Use mixins only from `ds/mixins.scss`. Avoid legacy mixins like - `@include flexCenter;`. Write standard CSS (flex/grid) instead. -- Use the `@use` instead of `@import`. If you go to refactor existing SCSS file, - try to replace all `@import` with `@use`. Example: `@use "ds/_sizes.scss" as - *;` (Use `as *` to expose variables directly). -- Avoid deep selector nesting or high-specificity (IDs). Flatten selectors: - - Avoid: `.card { .title { ... } }` - - Prefer: `.card-title { ... }` -- Leverage component-level CSS variables for state changes (hover/focus) instead - of rewriting properties. - -##### Checklist - -- [ ] No references to `common/refactor/` -- [ ] All `@import` converted to `@use` (only if refactoring) -- [ ] Physical properties (left/right) using logical properties (inline-start/end). -- [ ] Typography implemented via `use-typography()` mixin. -- [ ] Hardcoded pixel values wrapped in `px2rem()`. -- [ ] Selectors are flat (no deep nesting). - - -### Translations (`tr`) and Memoization - -`(tr "some.key")` resolves the translation string from the **currently active -locale at call time**. This has two consequences: - -- **Never call `(tr ...)` at namespace level** (inside a `def` or `defonce`). - Doing so would freeze the label to the locale active at module load time and - break runtime language switching. -- **Always call `(tr ...)` at render time** — either directly in the component - body or inside a `mf/with-memo` / `mf/use-memo` block. - -When a component renders a **static list of options** whose labels come from -`(tr ...)` (e.g. radio button options, select options), wrap the vector in -`mf/with-memo []` with no dependencies. This ensures the vector and its -`(tr ...)` calls are evaluated once per component mount instead of on every -render, while still respecting the render-time requirement: - -```clojure -(let [options (mf/with-memo [] - [{:value "top" :label (tr "some.key.top")} - {:value "center" :label (tr "some.key.center")} - {:value "bottom" :label (tr "some.key.bottom")}])] - ...) -``` - -### 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 -``` - -### Configuration - -`src/app/config.clj` reads globally defined variables and exposes precomputed -configuration values ready to be used from other parts of the application. - diff --git a/render-wasm/AGENTS.md b/render-wasm/AGENTS.md deleted file mode 100644 index 511b7da0c9..0000000000 --- a/render-wasm/AGENTS.md +++ /dev/null @@ -1,62 +0,0 @@ -# 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 corresponding ClojureScript bridge.