mirror of
https://github.com/penpot/penpot.git
synced 2026-03-13 22:08:27 +00:00
📚 Add GitHub Copilot instructions (#8548)
This commit is contained in:
42
.github/workflows/tests.yml
vendored
42
.github/workflows/tests.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -35,6 +35,7 @@
|
||||
/notes
|
||||
/playground/
|
||||
/backend/*.md
|
||||
!/backend/AGENTS.md
|
||||
/backend/*.sql
|
||||
/backend/*.txt
|
||||
/backend/assets/
|
||||
|
||||
265
AGENTS.md
Normal file
265
AGENTS.md
Normal 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
87
backend/AGENTS.md
Normal 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)`.
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
370
pnpm-lock.yaml
generated
Normal 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
61
render-wasm/AGENTS.md
Normal 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.
|
||||
50
run-ci.sh
50
run-ci.sh
@@ -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
|
||||
Reference in New Issue
Block a user