mirror of
https://github.com/penpot/penpot.git
synced 2026-06-09 08:52:05 +00:00
⏪ Backport from develop AGENTS.md changes
This commit is contained in:
parent
0e16db66b8
commit
51a9eed02e
@ -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/<cmd-name>`.
|
||||
|
||||
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 <table> -- 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"
|
||||
```
|
||||
@ -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"}`).
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
Loading…
x
Reference in New Issue
Block a user