6.4 KiB
radicle-desktop
Tauri desktop app for Radicle. Svelte 5 frontend communicates with a Rust
backend via Tauri IPC commands. TypeScript types are generated from Rust structs
using ts-rs (crates/radicle-types/).
The app lives in desktop/ of the radicle-gui monorepo. It is a package in the
root npm workspace and its own Cargo workspace: install npm dependencies at the
repository root with npm ci -w desktop, and run every other command here from
desktop/. All paths in this file are relative to it. Conventions shared with
explorer are in the root AGENTS.md, and UI code shared with it lives in
shared/ui/.
Svelte syntax and aliases
- Svelte 5 with runes syntax (
$state,$derived,$props,$effect) - Path aliases:
@app(src),@bindings(crates/radicle-types/bindings),@tests
Commands
Development
npm run dev # full Tauri dev build with hot reload and devtools
npm run start:http # test-http-api backend on :8081 (no Tauri runtime)
npm start # Vite dev server; in a browser it talks to test-http-api
Set VITE_LOG_INVOKE_TIMINGS=true to log each command's duration
([invoke] <command> <ms>ms) to the browser console. It is off by default.
Checks and linting
npm run check # svelte-check, eslint, prettier, then scripts/check-rs
npm run format # auto-fix prettier issues
Unit tests
npm run test:unit
DOMPurify does not sanitize under happy-dom (the unit-test environment):
<script> and onerror= pass through, and setConfig allowlists are ignored.
Don't assert sanitization in tests/unit/; verify it in the real app or in an
E2E test.
E2E tests
npm run test:e2e
npm run test:e2e -- tests/e2e/<file>.spec.ts
Run ./scripts/install-binaries once after cloning and whenever the Radicle
binaries are updated.
Peer keys have no passphrase, so the app authenticates without an ssh-agent.
Tests that need a running node use the peer fixture; tests that need an
ssh-agent use the sshAuthSock fixture and create a peer with a passphrase.
When a fixture depends on one peer seeing another's changes, wait for the node
event with waitForEvent instead of sleeping. Reload pages with reload(page)
from @tests/support/fixtures.js, not page.reload(), and navigate a page that
already shows the app with goto(page, url): the app polls the backend, and the
harness would otherwise fail the test on the request the navigation cancels.
Tests run against a production build in tests/tmp/build, served by
vite preview; the build takes a few seconds at startup. Set E2E_DEV_SERVER=1
to run against the Vite dev server instead, e.g. for hot reload while writing
tests.
Each run writes tests/artifacts/results.json (Playwright's JSON report) and
per-test logs in tests/artifacts/<test>/. Read those to see what failed in a
run someone else started.
SKIP_FIXTURE_CREATION=true skips fixture creation for faster iteration. Only
use it when you are solely editing .spec.ts files and fixtures already exist
from a previous full run. Any change to app code, the Rust backend, or test
fixtures requires a full run.
SKIP_FIXTURE_CREATION=true npm run test:e2e
Rust backend (crates/)
scripts/check-rs # cargo fmt --check, clippy on all targets, test
Regenerate TypeScript bindings
Run after changing any type annotated with #[derive(TS)] in
crates/radicle-types/, and commit the regenerated bindings:
npm run generate-types
Changes across layers
Adding or changing a Tauri command touches these layers in order:
crates/radicle-types/src/— add/update Rust types with#[derive(TS)]and#[ts(export)], runnpm run generate-typesto update@bindingscrates/radicle-types/src/traits/— add the method to the relevant port trait; default implementation goes here, not in the command handlercrates/radicle-tauri/src/commands/— add the#[tauri::command]handler (thin wrapper:ctx.method()), register incrates/radicle-tauri/src/lib.rscrates/test-http-api/src/— mirror the same route so E2E tests keep working;cargo test -p test-http-api --test parityfails until you do. A command that can't work over HTTP goes inTAURI_ONLYincrates/test-http-api/tests/parity.rs, with the reasonsrc/— call viainvoke<T>("command_name", args), import types from@bindings
Tests for a feature
When adding tests for a feature, go through docs/testing-checklist.md and
report which items are covered and which don't apply. The testing strategy is in
docs/adr/0001-testing-strategy.md.
Pre-push checklist
npm run check:ci runs everything CI runs on every PR (checks, unit tests,
install-binaries, E2E tests) and stops at the first failure. It writes the
full output, with each step's duration and exit code, to
tests/tmp/check-ci.log.
Backend architecture
Three crates in a hexagonal architecture; the reasoning is in
docs/adr/0002-backend-architecture.md.
radicle-types— domain types, ports (src/traits/), domain services (src/domain/), the SQLite adapter (src/outbound/), ts-rs bindingsradicle-tauri— Tauri driver;AppStateimplements the portstest-http-api— HTTP driver for E2E tests andnpm run start:http;Contextimplements the same ports
Ports have full default implementations, so a driver only implements Profile.
Paginated list views (issues, patches, notifications) go through a domain
Service backed by the SQLite adapter instead of the ports; new lists over
large data sets should do the same.
Code conventions
Svelte components
- Props use
$props():const { foo, bar = undefined }: Props = $props()
TypeScript
- Import backend types from
@bindings/*— these are generated by ts-rs, never hand-write interfaces that duplicate them - Call Tauri commands via
invoke<T>("command_name", { arg })from@app/lib/invoke
Rust backend
- All serialized types live in
crates/radicle-types/src/; annotate new types with#[derive(Serialize, TS)],#[serde(rename_all = "camelCase")],#[ts(export)], and#[ts(export_to = "<dir>/")] - Optional fields:
#[serde(skip_serializing_if = "Option::is_none")] - Error type:
radicle_types::error::Error rust-toolchain.tomltracks heartwood's. When bumping theradiclecrate, check heartwood's toolchain too: an older rustc fails inside the dependency with a confusing error (e.g. E0382 incob/identity.rsfor radicle 0.25)