Add improvements to AGENTS.md (#8586)

This commit is contained in:
Andrey Antukh
2026-03-11 15:24:40 +01:00
committed by GitHub
parent e855907b05
commit 7ec9261475
19 changed files with 1038 additions and 664 deletions

View File

@@ -34,6 +34,8 @@ jobs:
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:clj
pnpm run check-fmt:js
pnpm run lint:clj
- name: Lint Frontend
@@ -42,6 +44,9 @@ jobs:
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt:js
pnpm run check-fmt:clj
pnpm run check-fmt:scss
pnpm run lint:clj
pnpm run lint:js
pnpm run lint:scss
@@ -52,7 +57,8 @@ jobs:
corepack enable;
corepack install;
pnpm install;
pnpm run lint:clj
pnpm run check-fmt
pnpm run lint
- name: Lint Exporter
working-directory: ./exporter
@@ -60,7 +66,8 @@ jobs:
corepack enable;
corepack install;
pnpm install;
pnpm run lint:clj
pnpm run check-fmt
pnpm run lint
- name: Lint Library
working-directory: ./library
@@ -68,7 +75,8 @@ jobs:
corepack enable;
corepack install;
pnpm install;
pnpm run lint:clj
pnpm run check-fmt
pnpm run lint
test-common:
name: "Common Tests"
@@ -79,12 +87,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run tests on JVM
working-directory: ./common
run: |
clojure -M:dev:test
- name: Run tests on NODE
- name: Run tests
working-directory: ./common
run: |
./scripts/test

22
.gitignore vendored
View File

@@ -1,11 +1,4 @@
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.pnpm-store
*-init.clj
*.css.json
*.jar
@@ -20,8 +13,6 @@
.nyc_output
.rebel_readline_history
.repl
.shadow-cljs
.pnpm-store/
/*.jpg
/*.md
/*.png
@@ -36,6 +27,7 @@
/playground/
/backend/*.md
!/backend/AGENTS.md
/backend/.shadow-cljs
/backend/*.sql
/backend/*.txt
/backend/assets/
@@ -48,13 +40,13 @@
/backend/experiments
/backend/scripts/_env.local
/bundle*
/cd.md
/clj-profiler/
/common/coverage
/common/target
/deploy
/common/.shadow-cljs
/docker/images/bundle*
/exporter/target
/exporter/.shadow-cljs
/frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html
/frontend/playwright-report/
@@ -68,9 +60,9 @@
/frontend/storybook-static/
/frontend/target/
/frontend/test-results/
/frontend/.shadow-cljs
/other/
/scripts/
/telemetry/
/nexus/
/tmp/
/vendor/**/target
/vendor/svgclean/bundle*.js
@@ -79,13 +71,11 @@
/library/*.zip
/external
/penpot-nitrate
clj-profiler/
node_modules
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/render-wasm/target/
/**/node_modules
/**/.yarn/*
/.pnpm-store

352
AGENTS.md
View File

@@ -1,4 +1,4 @@
# Penpot Copilot Instructions
# Penpot Instructions
## Architecture Overview
@@ -18,7 +18,13 @@ The monorepo is managed with `pnpm` workspaces. The `manage.sh`
orchestrates cross-component builds. `run-ci.sh` defines the CI
pipeline.
---
## Search Standards
When searching code, always use `ripgrep` (rg) instead of grep if
available, as it respects `.gitignore` by default.
If using grep, try to exclude node_modules and .shadow-cljs directories
## Build, Test & Lint Commands
@@ -28,27 +34,26 @@ Run `./scripts/setup` for setup all dependencies.
```bash
# Dev
pnpm run watch:app # Full dev build (WASM + CLJS + assets)
# Production Build
# Build (Producution)
./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
pnpm run test # Build ClojureScript tests + run node target/tests/test.js
# 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
pnpm run lint:js # Linter for JS/TS
pnpm run lint:clj # Linter for CLJ/CLJS/CLJC
pnpm run lint:scss # Linter for SCSS
# Code formatting
pnpm run fmt:clj # Format CLJ
pnpm run fmt:js # prettier for JS
pnpm run fmt:scss # prettier for SCSS
# Check Code Formart
pnpm run check-fmt:clj # Format CLJ/CLJS/CLJC
pnpm run check-fmt:js # Format JS/TS
pnpm run check-fmt:scss # Format SCSS
# Code Format (Automatic Formating)
pnpm run fmt:clj # Format CLJ/CLJS/CLJC
pnpm run fmt:js # Format JS/TS
pnpm run fmt:scss # Format SCSS
```
To run a focused ClojureScript unit test: edit
@@ -58,28 +63,63 @@ 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
Run `pnpm install` for install all dependencies.
# Lint / Format
pnpm run lint:clj
pnpm run fmt:clj
```bash
# Run full test suite
pnpm run test
# Run single namespace
pnpm run test --focus backend-tests.rpc-doc-test
# Check Code Format
pnpm run check-fmt
# Code Format (Automatic Formatting)
pnpm run fmt
# Code Linter
pnpm run lint
```
Test config is in `backend/tests.edn`; test namespaces match `.*-test$` under `test/`.
Test config is in `backend/tests.edn`; test namespaces match
`.*-test$` under `test/` directory. You should not touch this file,
just use it for reference.
### Common (`cd common`)
This contains code that should compile and run under different runtimes: JVM & JS so the commands are
separarated for each runtime.
```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
clojure -M:dev:test # Run full test suite under JVM
clojure -M:dev:test --focus backend-tests.my-ns-test # Run single namespace under JVM
# Run full test suite under JS or JVM runtimes
pnpm run test:js
pnpm run test:jvm
# Run single namespace (only on JVM)
pnpm run test:jvm --focus common-tests.my-ns-test
# Lint
pnpm run lint:clj # Lint CLJ/CLJS/CLJC code
# Check Format
pnpm run check-fmt:clj # Check CLJ/CLJS/CLJS code
pnpm run check-fmt:js # Check JS/TS code
# Code Format (Automatic Formatting)
pnpm run fmt:clj # Check CLJ/CLJS/CLJS code
pnpm run fmt:js # Check JS/TS code
```
To run a focused ClojureScript unit test: edit
`test/common_tests/runner.cljs` to narrow the test suite, then `pnpm
run build:test && node target/tests/test.js`.
### Render-WASM (`cd render-wasm`)
```bash
@@ -93,6 +133,10 @@ cargo fmt --check
### Namespace Structure
The backend, frontend and exporter are developed using clojure and
clojurescript and code is organized in namespaces. This is a general
overview of the available namespaces.
**Backend:**
- `app.rpc.commands.*` RPC command implementations (`auth`, `files`, `teams`, etc.)
- `app.http.*` HTTP routes and middleware
@@ -109,14 +153,26 @@ cargo fmt --check
- `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.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 around all application
- `app.common.math` Generic math helpers used around all aplication
- `app.common.json` Generic JSON encoding/decoding helpers
- `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>`.
The PRC methods are implement in a some kind of multimethod structure using
`app.util.serivices` namespace. All RPC methods are collected under `app.rpc`
namespace and exposed under `/api/rpc/command/<cmd-name>`. The RPC method
accepts POST and GET requests indistinctly and uses `Accept` header for
negotiate the response encoding (which can be transit, the defaut or plain
json). It also accepts transit (defaut) or json as input, which should be
indicated using `Content-Type` header.
This is an example:
```clojure
(sv/defmethod ::my-command
@@ -129,12 +185,18 @@ All API calls go through a single RPC endpoint: `POST /api/rpc/command/<cmd-name
{:id (uuid/next)})
```
Look under `src/app/rpc/commands/*.clj` to see more examples.
### Frontend State Management (Potok)
State is a single atom managed by a Potok store. Events implement protocols:
State is a single atom managed by a Potok store. Events implement protocols
(funcool/potok library):
```clojure
(defn my-event [data]
(defn my-event
"doc string"
[data]
(ptk/reify ::my-event
ptk/UpdateEvent
(update [_ state] ;; synchronous state transition
@@ -148,19 +210,40 @@ State is a single atom managed by a Potok store. Events implement protocols:
ptk/EffectEvent
(effect [_ state _] ;; pure side effects (DOM, logging)
(.focus (dom/get-element "id")))))
(dom/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
The state is located under `app.main.store` namespace where we have
the `emit!` function responsible of emiting 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 lookup into the main state
for just inner data or precalculated data. That references are very usefull but
should be used with care because, per example if we have complex operation, this
operation will be executed on each state change, and sometimes 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 for handling it and the use the
new helper.
### CSS Modules Pattern
### CSS (Modules Pattern)
Styles are co-located with components. Each `.cljs` file has a corresponding `.scss` file:
Styles are co-located with components. Each `.cljs` file has a corresponding
`.scss` file:
```clojure
;; In the component namespace:
@@ -174,8 +257,24 @@ Styles are co-located with components. Each `.cljs` file has a corresponding `.s
;; When you need concat an existing class:
[:div {:class [existing-class (stl/css-case :some-class true :selected (= drawtool :rect))]}]
```
### Integration tests (Playwright)
Integration tests are developed under `frontend/playwright` directory, we use
mocks for remove communication with backend.
The tests should be executed under `./frontend` directory:
```
cd frontend/
pnpm run test:e2e # Playwright e2e tests
pnpm run test:e2e --grep "pattern" # Single e2e test by pattern
```
Ensure everything installed with `./scripts/setup` script.
### Performance Macros (`app.common.data.macros`)
@@ -187,7 +286,7 @@ Always prefer these macros over their `clojure.core` equivalents — they compil
(dm/str "a" "b" "c") ;; string concatenation
```
### Shared Code (cljc)
### Shared Code under Common (CLJC)
Files in `common/src/app/common/` use reader conditionals to target both runtimes:
@@ -196,37 +295,129 @@ Files in `common/src/app/common/` use reader conditionals to target both runtime
:cljs (:require [cljs.core :as core]))
```
Both frontend and backend depend on `common` as a local library (`penpot/common {:local/root "../common"}`).
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.
### Component Standards & Syntax (React & Rumext: mf/defc)
```clojure
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]} ;; React.memo
[{:keys [name on-click]}]
{::mf/wrap [mf/memo]} ;; Equivalent to React.memo
[{:keys [name on-click]}] ;; Destructured props
[:div {:class (stl/css :root)
:on-click on-click}
name])
```
Hooks: `(mf/use-state)`, `(mf/use-effect)`, `(mf/use-memo)` analgous to react hooks.
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!` for to perform an update and
`deref` for get the current value.
You also has `mf/deref` hook (which does not follow the `use-` naming pattern)
and it's purpose is watch (subscribe to changes) on atom or derived atom (from
okulary) and get the current value. Is mainly used for 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:
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.
Example for `mf/with-memo` 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)))))
## Commit Guidelines
;; 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 it 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])
```
4. Checklist
- [ ] Does the component name end with *?
## Commit Format Guidelines
Format: `<emoji-code> <subject>`
@@ -263,3 +454,46 @@ applicable.
| ⬇️ | `:arrow_down:` | Dependency downgrade |
| 🔥 | `:fire:` | Remove files or code |
| 🌐 | `:globe_with_meridians:` | Translations |
## SCSS Rules & Migration
### General rules
- 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.
- ❌ `margin-left`, `padding-right`, `left`, `right`.
- ✅ `margin-inline-start`, `padding-inline-end`, `inset-inline-start`.
- Always use the `use-typography` mixin from `ds/typography.scss`.
- ✅ `@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 those defined in`ds/mixins.scss`. Avoid legacy mixins like
`@include flexCenter;`. Write standard CSS (flex/grid) instead.
### Syntax & Structure
- 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:
- ❌ `.card { .title { ... } }`
- ✅ `.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).

View File

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

View File

@@ -13,6 +13,7 @@
"devDependencies": {
"concurrently": "^9.1.2",
"nodemon": "^3.1.10",
"prettier": "3.5.3",
"source-map-support": "^0.5.21",
"ws": "^8.18.2"
},
@@ -20,11 +21,15 @@
"date-fns": "^4.1.0"
},
"scripts": {
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel=true --lint src/",
"lint:clj": "clj-kondo --parallel=true --lint src/",
"check-fmt:clj": "cljfmt check --parallel=true src/ test/",
"check-fmt:js": "prettier -c src/**/*.js",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"fmt:js": "prettier -c src/**/*.js -w",
"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",
"test": "pnpm run build:test && node target/tests/test.js"
"test:js": "pnpm run build:test && node target/tests/test.js",
"test:jvm": "clojure -M:dev:test"
}
}

10
common/pnpm-lock.yaml generated
View File

@@ -18,6 +18,9 @@ importers:
nodemon:
specifier: ^3.1.10
version: 3.1.11
prettier:
specifier: 3.5.3
version: 3.5.3
source-map-support:
specifier: ^0.5.21
version: 0.5.21
@@ -169,6 +172,11 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
prettier@3.5.3:
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
engines: {node: '>=14'}
hasBin: true
pstree.remy@1.1.8:
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
@@ -405,6 +413,8 @@ snapshots:
picomatch@2.3.1: {}
prettier@3.5.3: {}
pstree.remy@1.1.8: {}
readdirp@3.6.0:

View File

@@ -4,4 +4,5 @@ set -ex
corepack enable;
corepack install;
pnpm install;
pnpm run test;
pnpm run test:js;
pnpm run test:jvm;

View File

@@ -10,7 +10,7 @@
goog.require("cljs.core");
goog.provide("app.common.encoding_impl");
goog.scope(function() {
goog.scope(function () {
const core = cljs.core;
const global = goog.global;
const self = app.common.encoding_impl;
@@ -28,8 +28,10 @@ goog.scope(function() {
// Accept UUID hex format
input = input.replace(/-/g, "");
if ((input.length % 2) !== 0) {
throw new RangeError("Expected string to be an even number of characters")
if (input.length % 2 !== 0) {
throw new RangeError(
"Expected string to be an even number of characters",
);
}
const view = new Uint8Array(input.length / 2);
@@ -44,7 +46,11 @@ goog.scope(function() {
function bufferToHex(source, isUuid) {
if (source instanceof Uint8Array) {
} else if (ArrayBuffer.isView(source)) {
source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
source = new Uint8Array(
source.buffer,
source.byteOffset,
source.byteLength,
);
} else if (Array.isArray(source)) {
source = Uint8Array.from(source);
}
@@ -56,22 +62,28 @@ goog.scope(function() {
const spacer = isUuid ? "-" : "";
let i = 0;
return (hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] + spacer +
hexMap[source[i++]] +
hexMap[source[i++]] + spacer +
hexMap[source[i++]] +
hexMap[source[i++]] + spacer +
hexMap[source[i++]] +
hexMap[source[i++]] + spacer +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]]);
return (
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
spacer +
hexMap[source[i++]] +
hexMap[source[i++]] +
spacer +
hexMap[source[i++]] +
hexMap[source[i++]] +
spacer +
hexMap[source[i++]] +
hexMap[source[i++]] +
spacer +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]] +
hexMap[source[i++]]
);
}
self.hexToBuffer = hexToBuffer;
@@ -87,8 +99,10 @@ goog.scope(function() {
// for base16 (hex), base32, or base64 encoding in a standards
// compliant manner.
function getBaseCodec (ALPHABET) {
if (ALPHABET.length >= 255) { throw new TypeError("Alphabet too long"); }
function getBaseCodec(ALPHABET) {
if (ALPHABET.length >= 255) {
throw new TypeError("Alphabet too long");
}
let BASE_MAP = new Uint8Array(256);
for (let j = 0; j < BASE_MAP.length; j++) {
BASE_MAP[j] = 255;
@@ -96,22 +110,32 @@ goog.scope(function() {
for (let i = 0; i < ALPHABET.length; i++) {
let x = ALPHABET.charAt(i);
let xc = x.charCodeAt(0);
if (BASE_MAP[xc] !== 255) { throw new TypeError(x + " is ambiguous"); }
if (BASE_MAP[xc] !== 255) {
throw new TypeError(x + " is ambiguous");
}
BASE_MAP[xc] = i;
}
let BASE = ALPHABET.length;
let LEADER = ALPHABET.charAt(0);
let FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up
let iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up
function encode (source) {
function encode(source) {
if (source instanceof Uint8Array) {
} else if (ArrayBuffer.isView(source)) {
source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
source = new Uint8Array(
source.buffer,
source.byteOffset,
source.byteLength,
);
} else if (Array.isArray(source)) {
source = Uint8Array.from(source);
}
if (!(source instanceof Uint8Array)) { throw new TypeError("Expected Uint8Array"); }
if (source.length === 0) { return ""; }
if (!(source instanceof Uint8Array)) {
throw new TypeError("Expected Uint8Array");
}
if (source.length === 0) {
return "";
}
// Skip & count leading zeroes.
let zeroes = 0;
let length = 0;
@@ -129,12 +153,18 @@ goog.scope(function() {
let carry = source[pbegin];
// Apply "b58 = b58 * 256 + ch".
let i = 0;
for (let it1 = size - 1; (carry !== 0 || i < length) && (it1 !== -1); it1--, i++) {
for (
let it1 = size - 1;
(carry !== 0 || i < length) && it1 !== -1;
it1--, i++
) {
carry += (256 * b58[it1]) >>> 0;
b58[it1] = (carry % BASE) >>> 0;
b58[it1] = carry % BASE >>> 0;
carry = (carry / BASE) >>> 0;
}
if (carry !== 0) { throw new Error("Non-zero carry"); }
if (carry !== 0) {
throw new Error("Non-zero carry");
}
length = i;
pbegin++;
}
@@ -145,13 +175,19 @@ goog.scope(function() {
}
// Translate the result into a string.
let str = LEADER.repeat(zeroes);
for (; it2 < size; ++it2) { str += ALPHABET.charAt(b58[it2]); }
for (; it2 < size; ++it2) {
str += ALPHABET.charAt(b58[it2]);
}
return str;
}
function decodeUnsafe (source) {
if (typeof source !== "string") { throw new TypeError("Expected String"); }
if (source.length === 0) { return new Uint8Array(); }
function decodeUnsafe(source) {
if (typeof source !== "string") {
throw new TypeError("Expected String");
}
if (source.length === 0) {
return new Uint8Array();
}
let psz = 0;
// Skip and count leading '1's.
let zeroes = 0;
@@ -161,21 +197,29 @@ goog.scope(function() {
psz++;
}
// Allocate enough space in big-endian base256 representation.
let size = (((source.length - psz) * FACTOR) + 1) >>> 0; // log(58) / log(256), rounded up.
let size = ((source.length - psz) * FACTOR + 1) >>> 0; // log(58) / log(256), rounded up.
let b256 = new Uint8Array(size);
// Process the characters.
while (source[psz]) {
// Decode character
let carry = BASE_MAP[source.charCodeAt(psz)];
// Invalid character
if (carry === 255) { return; }
if (carry === 255) {
return;
}
let i = 0;
for (let it3 = size - 1; (carry !== 0 || i < length) && (it3 !== -1); it3--, i++) {
for (
let it3 = size - 1;
(carry !== 0 || i < length) && it3 !== -1;
it3--, i++
) {
carry += (BASE * b256[it3]) >>> 0;
b256[it3] = (carry % 256) >>> 0;
b256[it3] = carry % 256 >>> 0;
carry = (carry / 256) >>> 0;
}
if (carry !== 0) { throw new Error("Non-zero carry"); }
if (carry !== 0) {
throw new Error("Non-zero carry");
}
length = i;
psz++;
}
@@ -192,20 +236,22 @@ goog.scope(function() {
return vch;
}
function decode (string) {
function decode(string) {
let buffer = decodeUnsafe(string);
if (buffer) { return buffer; }
if (buffer) {
return buffer;
}
throw new Error("Non-base" + BASE + " character");
}
return {
encode: encode,
decodeUnsafe: decodeUnsafe,
decode: decode
decode: decode,
};
}
// MORE bases here: https://github.com/cryptocoinjs/base-x/tree/master
const BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const BASE62 =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
self.bufferToBase62 = getBaseCodec(BASE62).encode;
});

View File

@@ -14,7 +14,7 @@
goog.provide("app.common.svg.path.arc_to_bezier");
// https://raw.githubusercontent.com/fontello/svgpath/master/lib/a2c.js
goog.scope(function() {
goog.scope(function () {
const self = app.common.svg.path.arc_to_bezier;
var TAU = Math.PI * 2;
@@ -27,20 +27,23 @@ goog.scope(function() {
// we can use simplified math (without length normalization)
//
function unit_vector_angle(ux, uy, vx, vy) {
var sign = (ux * vy - uy * vx < 0) ? -1 : 1;
var dot = ux * vx + uy * vy;
var sign = ux * vy - uy * vx < 0 ? -1 : 1;
var dot = ux * vx + uy * vy;
// Add this to work with arbitrary vectors:
// dot /= Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
// rounding errors, e.g. -1.0000000000000002 can screw up this
if (dot > 1.0) { dot = 1.0; }
if (dot < -1.0) { dot = -1.0; }
if (dot > 1.0) {
dot = 1.0;
}
if (dot < -1.0) {
dot = -1.0;
}
return sign * Math.acos(dot);
}
// Convert from endpoint to center parameterization,
// see http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
//
@@ -53,11 +56,11 @@ goog.scope(function() {
// points. After that, rotate it to line up ellipse axes with coordinate
// axes.
//
var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;
var x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2;
var y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2;
var rx_sq = rx * rx;
var ry_sq = ry * ry;
var rx_sq = rx * rx;
var ry_sq = ry * ry;
var x1p_sq = x1p * x1p;
var y1p_sq = y1p * y1p;
@@ -66,33 +69,33 @@ goog.scope(function() {
// Compute coordinates of the centre of this ellipse (cx', cy')
// in the new coordinate system.
//
var radicant = (rx_sq * ry_sq) - (rx_sq * y1p_sq) - (ry_sq * x1p_sq);
var radicant = rx_sq * ry_sq - rx_sq * y1p_sq - ry_sq * x1p_sq;
if (radicant < 0) {
// due to rounding errors it might be e.g. -1.3877787807814457e-17
radicant = 0;
}
radicant /= (rx_sq * y1p_sq) + (ry_sq * x1p_sq);
radicant /= rx_sq * y1p_sq + ry_sq * x1p_sq;
radicant = Math.sqrt(radicant) * (fa === fs ? -1 : 1);
var cxp = radicant * rx/ry * y1p;
var cyp = radicant * -ry/rx * x1p;
var cxp = ((radicant * rx) / ry) * y1p;
var cyp = ((radicant * -ry) / rx) * x1p;
// Step 3.
//
// Transform back to get centre coordinates (cx, cy) in the original
// coordinate system.
//
var cx = cos_phi*cxp - sin_phi*cyp + (x1+x2)/2;
var cy = sin_phi*cxp + cos_phi*cyp + (y1+y2)/2;
var cx = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2;
var cy = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2;
// Step 4.
//
// Compute angles (theta1, delta_theta).
//
var v1x = (x1p - cxp) / rx;
var v1y = (y1p - cyp) / ry;
var v1x = (x1p - cxp) / rx;
var v1y = (y1p - cyp) / ry;
var v2x = (-x1p - cxp) / rx;
var v2y = (-y1p - cyp) / ry;
@@ -106,7 +109,7 @@ goog.scope(function() {
delta_theta += TAU;
}
return [ cx, cy, theta1, delta_theta ];
return [cx, cy, theta1, delta_theta];
}
//
@@ -114,24 +117,33 @@ goog.scope(function() {
// see http://math.stackexchange.com/questions/873224
//
function approximate_unit_arc(theta1, delta_theta) {
var alpha = 4/3 * Math.tan(delta_theta/4);
var alpha = (4 / 3) * Math.tan(delta_theta / 4);
var x1 = Math.cos(theta1);
var y1 = Math.sin(theta1);
var x2 = Math.cos(theta1 + delta_theta);
var y2 = Math.sin(theta1 + delta_theta);
return [ x1, y1, x1 - y1*alpha, y1 + x1*alpha, x2 + y2*alpha, y2 - x2*alpha, x2, y2 ];
return [
x1,
y1,
x1 - y1 * alpha,
y1 + x1 * alpha,
x2 + y2 * alpha,
y2 - x2 * alpha,
x2,
y2,
];
}
function calculate_beziers(x1, y1, x2, y2, fa, fs, rx, ry, phi) {
var sin_phi = Math.sin(phi * TAU / 360);
var cos_phi = Math.cos(phi * TAU / 360);
var sin_phi = Math.sin((phi * TAU) / 360);
var cos_phi = Math.cos((phi * TAU) / 360);
// Make sure radii are valid
//
var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;
var x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2;
var y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2;
// console.log("L", x1p, y1p)
@@ -145,7 +157,6 @@ goog.scope(function() {
return [];
}
// Compensate out-of-range radii
//
rx = Math.abs(rx);
@@ -157,25 +168,20 @@ goog.scope(function() {
ry *= Math.sqrt(lambda);
}
// Get center parameters (cx, cy, theta1, delta_theta)
//
var cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi);
var result = [];
var theta1 = cc[2];
var delta_theta = cc[3];
// Split an arc to multiple segments, so each segment
// will be less than τ/4 (= 90°)
//
var segments = Math.max(Math.ceil(Math.abs(delta_theta) / (TAU / 4)), 1);
delta_theta /= segments;
for (var i = 0; i < segments; i++) {
var item = approximate_unit_arc(theta1, delta_theta);
result.push(item);
@@ -195,8 +201,8 @@ goog.scope(function() {
y *= ry;
// rotate
var xp = cos_phi*x - sin_phi*y;
var yp = sin_phi*x + cos_phi*y;
var xp = cos_phi * x - sin_phi * y;
var yp = sin_phi * x + cos_phi * y;
// translate
curve[i + 0] = xp + cc[0];

File diff suppressed because it is too large Load Diff

View File

@@ -10,16 +10,18 @@
goog.require("app.common.encoding_impl");
goog.provide("app.common.uuid_impl");
goog.scope(function() {
goog.scope(function () {
const global = goog.global;
const encoding = app.common.encoding_impl;
const encoding = app.common.encoding_impl;
const self = app.common.uuid_impl;
const timeRef = 1640995200000; // ms since 2022-01-01T00:00:00
const fill = (() => {
if (typeof global.crypto !== "undefined" &&
typeof global.crypto.getRandomValues !== "undefined") {
if (
typeof global.crypto !== "undefined" &&
typeof global.crypto.getRandomValues !== "undefined"
) {
return (buf) => {
global.crypto.getRandomValues(buf);
return buf;
@@ -30,7 +32,7 @@ goog.scope(function() {
return (buf) => {
const bytes = randomBytes(buf.length);
buf.set(bytes)
buf.set(bytes);
return buf;
};
} else {
@@ -39,8 +41,10 @@ goog.scope(function() {
return (buf) => {
for (let i = 0, r; i < buf.length; i++) {
if ((i & 0x03) === 0) { r = Math.random() * 0x100000000; }
buf[i] = r >>> ((i & 0x03) << 3) & 0xff;
if ((i & 0x03) === 0) {
r = Math.random() * 0x100000000;
}
buf[i] = (r >>> ((i & 0x03) << 3)) & 0xff;
}
return buf;
};
@@ -50,31 +54,38 @@ goog.scope(function() {
function toHexString(buf) {
const hexMap = encoding.hexMap;
let i = 0;
return (hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]] + '-' +
hexMap[buf[i++]] +
hexMap[buf[i++]] + '-' +
hexMap[buf[i++]] +
hexMap[buf[i++]] + '-' +
hexMap[buf[i++]] +
hexMap[buf[i++]] + '-' +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]]);
};
return (
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
"-" +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
"-" +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
"-" +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
"-" +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]] +
hexMap[buf[i++]]
);
}
function getBigUint64(view, byteOffset, le) {
const a = view.getUint32(byteOffset, le);
const b = view.getUint32(byteOffset + 4, le);
const leMask = Number(!!le);
const beMask = Number(!le);
return ((BigInt(a * beMask + b * leMask) << 32n) |
(BigInt(a * leMask + b * beMask)));
return (
(BigInt(a * beMask + b * leMask) << 32n) | BigInt(a * leMask + b * beMask)
);
}
function setBigUint64(view, byteOffset, value, le) {
@@ -83,8 +94,7 @@ goog.scope(function() {
if (le) {
view.setUint32(byteOffset + 4, hi, le);
view.setUint32(byteOffset, lo, le);
}
else {
} else {
view.setUint32(byteOffset, hi, le);
view.setUint32(byteOffset + 4, lo, le);
}
@@ -104,17 +114,18 @@ goog.scope(function() {
}
self.shortID = (function () {
const buff = new ArrayBuffer(8);
const buff = new ArrayBuffer(8);
const int8 = new Uint8Array(buff);
const view = new DataView(buff);
const view = new DataView(buff);
const base = 0x0000_0000_0000_0000n;
return function shortID(ts) {
const tss = currentTimestamp(timeRef);
const msb = (base
| (nextLong() & 0xffff_ffff_0000_0000n)
| (tss & 0x0000_0000_ffff_ffffn));
const msb =
base |
(nextLong() & 0xffff_ffff_0000_0000n) |
(tss & 0x0000_0000_ffff_ffffn);
setBigUint64(view, 0, msb, false);
return encoding.toBase62(int8);
};
@@ -139,9 +150,9 @@ goog.scope(function() {
const maxCs = 0x0000_0000_0000_3fffn; // 14 bits space
let countCs = 0n;
let lastRd = 0n;
let lastCs = 0n;
let lastTs = 0n;
let lastRd = 0n;
let lastCs = 0n;
let lastTs = 0n;
let baseMsb = 0x0000_0000_0000_8000n;
let baseLsb = 0x8000_0000_0000_0000n;
@@ -149,12 +160,9 @@ goog.scope(function() {
lastCs = nextLong() & maxCs;
const create = function create(ts, lastRd, lastCs) {
const msb = (baseMsb
| (lastRd & 0xffff_ffff_ffff_0fffn));
const msb = baseMsb | (lastRd & 0xffff_ffff_ffff_0fffn);
const lsb = (baseLsb
| ((ts << 14n) & 0x3fff_ffff_ffff_c000n)
| lastCs);
const lsb = baseLsb | ((ts << 14n) & 0x3fff_ffff_ffff_c000n) | lastCs;
setBigUint64(view, 0, msb, false);
setBigUint64(view, 8, lsb, false);
@@ -167,10 +175,10 @@ goog.scope(function() {
let ts = currentTimestamp(timeRef);
// Protect from clock regression
if ((ts - lastTs) < 0) {
lastRd = (lastRd
& 0x0000_0000_0000_0f00n
| (nextLong() & 0xffff_ffff_ffff_f0ffn));
if (ts - lastTs < 0) {
lastRd =
(lastRd & 0x0000_0000_0000_0f00n) |
(nextLong() & 0xffff_ffff_ffff_f0ffn);
countCs = 0n;
continue;
}
@@ -209,63 +217,63 @@ goog.scope(function() {
// Parse ........-....-....-####-............
int8[8] = (rest = parseInt(uuid.slice(19, 23), 16)) >>> 8;
int8[9] = rest & 0xff,
// Parse ........-....-....-....-############
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
int8[10] = ((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff;
(int8[9] = rest & 0xff),
// Parse ........-....-....-....-############
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
(int8[10] =
((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff);
int8[11] = (rest / 0x100000000) & 0xff;
int8[12] = (rest >>> 24) & 0xff;
int8[13] = (rest >>> 16) & 0xff;
int8[14] = (rest >>> 8) & 0xff;
int8[15] = rest & 0xff;
}
};
const fromPair = (hi, lo) => {
view.setBigInt64(0, hi);
view.setBigInt64(8, lo);
return encoding.bufferToHex(int8, true);
}
};
const getHi = (uuid) => {
fillBytes(uuid);
return view.getBigInt64(0);
}
};
const getLo = (uuid) => {
fillBytes(uuid);
return view.getBigInt64(8);
}
};
const getBytes = (uuid) => {
fillBytes(uuid);
return Int8Array.from(int8);
}
};
const getUnsignedParts = (uuid) => {
fillBytes(uuid);
const result = new Uint32Array(4);
result[0] = view.getUint32(0)
result[0] = view.getUint32(0);
result[1] = view.getUint32(4);
result[2] = view.getUint32(8);
result[3] = view.getUint32(12);
return result;
}
};
const fromUnsignedParts = (a, b, c, d) => {
view.setUint32(0, a)
view.setUint32(4, b)
view.setUint32(8, c)
view.setUint32(12, d)
view.setUint32(0, a);
view.setUint32(4, b);
view.setUint32(8, c);
view.setUint32(12, d);
return encoding.bufferToHex(int8, true);
}
};
const fromArray = (u8data) => {
int8.set(u8data);
return encoding.bufferToHex(int8, true);
}
};
const setTag = (tag) => {
tag = BigInt.asUintN(64, "" + tag);
@@ -273,9 +281,9 @@ goog.scope(function() {
throw new Error("illegal arguments: tag value should fit in 4bits");
}
lastRd = (lastRd
& 0xffff_ffff_ffff_f0ffn
| ((tag << 8) & 0x0000_0000_0000_0f00n));
lastRd =
(lastRd & 0xffff_ffff_ffff_f0ffn) |
((tag << 8) & 0x0000_0000_0000_0f00n);
};
factory.create = create;
@@ -290,9 +298,9 @@ goog.scope(function() {
return factory;
})();
self.shortV8 = function(uuid) {
self.shortV8 = function (uuid) {
const buff = encoding.hexToBuffer(uuid);
const short = new Uint8Array(buff, 4);
const short = new Uint8Array(buff, 4);
return encoding.bufferToBase62(short);
};
@@ -307,7 +315,7 @@ goog.scope(function() {
return self.v8.fromPair(hi, lo);
};
self.fromBytes = function(data) {
self.fromBytes = function (data) {
if (data instanceof Uint8Array) {
return self.v8.fromArray(data);
} else if (data instanceof Int8Array) {
@@ -325,15 +333,15 @@ goog.scope(function() {
return self.v8.getUnsignedParts(uuid);
};
self.fromUnsignedParts = function(a,b,c,d) {
return self.v8.fromUnsignedParts(a,b,c,d);
self.fromUnsignedParts = function (a, b, c, d) {
return self.v8.fromUnsignedParts(a, b, c, d);
};
self.getHi = function (uuid) {
return self.v8.getHi(uuid);
}
};
self.getLo = function (uuid) {
return self.v8.getLo(uuid);
}
};
});

View File

@@ -67,8 +67,11 @@ export class WeakEqMap {
}
set(key, value) {
if (key === null || (typeof key !== 'object' && typeof key !== 'function')) {
throw new TypeError('WeakEqMap keys must be objects (like WeakMap).');
if (
key === null ||
(typeof key !== "object" && typeof key !== "function")
) {
throw new TypeError("WeakEqMap keys must be objects (like WeakMap).");
}
const hash = this._hash(key);
const bucket = this._getBucket(hash);

View File

@@ -1,4 +1,4 @@
#kaocha/v1
{:tests [{:id :unit
:test-paths ["test"]}]
:kaocha/reporter [kaocha.report/dots]}
{:tests [{:id :unit
:test-paths ["test"]}]
:kaocha/reporter [kaocha.report/dots]}

View File

@@ -18,6 +18,7 @@ RUN set -ex; \
curl \
bash \
git \
ripgrep \
\
curl \
ca-certificates \

View File

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

View File

@@ -23,12 +23,15 @@
"build:app:main": "clojure -M:dev:shadow-cljs release main worker",
"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",
"check-fmt:clj": "cljfmt check --parallel=true src/ test/",
"check-fmt:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js",
"check-fmt:scss": "prettier -c resources/styles -c src/**/*.scss",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"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",
"lint:clj": "clj-kondo --parallel --lint ../common/src src/",
"lint:js": "exit 0",
"lint:scss": "exit 0",
"build:test": "clojure -M:dev:shadow-cljs compile test",
"test": "pnpm run build:test && node target/tests/test.js",
"test:storybook": "vitest run --project=storybook",

View File

@@ -26,8 +26,9 @@
"clear:shadow-cache": "rm -rf .shadow-cljs",
"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/",
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel --lint src/",
"fmt": "cljfmt fix --parallel=true src/ test/",
"check-fmt": "cljfmt check --parallel=true src/ test/",
"lint": "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"

13
scripts/check-fmt Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -ex
cljfmt --parallel=true check \
common/src/ \
common/test/ \
frontend/src/ \
frontend/test/ \
backend/src/ \
backend/test/ \
exporter/src/ \
library/src;

View File

@@ -2,16 +2,6 @@
set -ex
cljfmt check --parallel=true \
common/src/ \
common/test/ \
frontend/src/ \
frontend/test/ \
backend/src/ \
backend/test/ \
exporter/src/ \
library/src;
clj-kondo --parallel=true --lint common/src;
clj-kondo --parallel=true --lint frontend/src;
clj-kondo --parallel=true --lint backend/src;