Repository navigation
Development Setup
English · Español
How to build, run and check Proxy Builder from source: prerequisites, the pnpm policy, every package.json script, the repository layout, the ts-rs bindings flow, WebKitGTK notes, git hooks and formatter/linter rules.
For what the checks do in CI see Testing and CI; for coding rules see Conventions.
| Tool | Version | Source of truth |
|---|---|---|
| Node.js | 22 or later |
package.json engines.node; CI uses Node 22 |
| pnpm | 12 or later; the exact version is pinned in packageManager (pnpm@12.14.0) |
package.json; npm is not supported |
| Rust (stable) | 1.92 or later (rust-version in src-tauri/Cargo.toml, crates/proxy-core/Cargo.toml and crates/mesa/Cargo.toml) |
CI uses the latest stable with clippy and rustfmt
|
| Node.js for the room server (optional) | 22.13 or later (it uses node:sqlite) |
server/package.json engines.node; only needed to run the server or its tests (see below) |
| Tauri system libraries (Linux) | WebKitGTK 4.1 and friends | see below |
| Tauri prerequisites (Windows, macOS) | the standard Tauri 2 ones (WebView2, MSVC build tools / Xcode command line tools) | Inferred from Tauri 2 defaults; the repo does not pin them |
Linux packages (Debian/Ubuntu), from README.md:
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev \
libssl-dev libayatana-appindicator3-dev librsvg2-devCI (.github/workflows/ci.yml) installs libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf on ubuntu-22.04 (cached, see Testing and CI). The app crate links GTK/WebKit, so cargo test/clippy for the whole workspace need those libraries even though you never open a window; the pure crates alone (cargo test -p proxy-core, cargo test -p mesa) do not.
Note: although libssl-dev is in the README list (Tauri's usual set), the dependency tree must contain no OpenSSL: reqwest is built with default-features = false and rustls, and CI fails if cargo tree -i openssl finds anything (see Testing and CI).
pnpm install
pnpm tauri devpnpm tauri dev runs beforeDevCommand (pnpm dev, i.e. Vite on port 1420, strictPort: true: it fails instead of picking another port) and compiles the Rust app. The first Rust build takes several minutes. Vite is told not to watch src-tauri/ (Cargo handles that); with TAURI_DEV_HOST set it listens on that host with HMR on port 1421 (for mobile/remote dev; see vite.config.ts).
pnpm tauri build produces installers for the current platform (bundle.targets: "all" in src-tauri/tauri.conf.json); signing for the updater needs the private key in the environment, see Releases and Auto-Updates.
Configuration for pnpm 11+ lives in pnpm-workspace.yaml, not in package.json:
| Key | Value | Why |
|---|---|---|
minimumReleaseAge |
1440 (minutes) |
Only install package versions published at least a day ago. This is pnpm 11's default; it is written down so every machine (cloud sandbox, maintainer's laptop) resolves the same way. A lockfile generated without the delay once contained versions published hours earlier and was rejected by the maintainer's own pnpm install. |
allowBuilds |
esbuild: true |
The only dependency allowed to run an install script. |
Rules that follow from this:
- Regenerate
pnpm-lock.yamlwith the pnpm version inpackageManager. A lockfile produced by pnpm 10 skips the age rule and can pull brand-new versions. - CI installs with
pnpm install --frozen-lockfile(bothci.ymlandrelease.yml). - Biome ignores
pnpm-lock.yamland lint-staged excludes it from JSON formatting.
All from package.json:
| Script | Runs | Use |
|---|---|---|
pnpm dev |
vite |
Frontend dev server only (Tauri calls it for you) |
pnpm build |
tsc && vite build |
Type-check then bundle into dist/ (frontendDist) |
pnpm preview |
vite preview |
Serve the built bundle |
pnpm tauri |
tauri |
Tauri CLI (pnpm tauri dev, pnpm tauri build, pnpm tauri icon) |
pnpm bindings |
cargo test --manifest-path src-tauri/Cargo.toml --workspace export_bindings |
Regenerate src/bindings/*.ts from Rust types (see below) |
pnpm check |
check:tipos, test, check:biome, check:title, idiomas, check:knip in that order |
Everything cheap on the frontend |
pnpm check:tipos |
tsc --noEmit |
Type check |
pnpm test |
vitest run |
Frontend and script unit tests |
pnpm check:biome |
biome check . |
Lint + format check |
pnpm check:title |
node scripts/sin-title-nativo.mjs |
Fails on native title= attributes |
pnpm idiomas |
node scripts/idiomas.mjs |
Fails if any interface language is missing a translation or has mismatched placeholders/plurals; node scripts/idiomas.mjs --detalle also lists the warnings (see Testing and CI) |
pnpm check:knip |
knip |
Unused files and dependencies |
pnpm check:clippy |
cargo clippy --manifest-path src-tauri/Cargo.toml --workspace --all-targets -- -D warnings |
Rust lints; warnings are errors |
pnpm check:rust-fmt |
cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check |
Rust format check |
pnpm test:rust |
cargo test --manifest-path src-tauri/Cargo.toml --workspace |
All Rust tests (app, proxy-core, mesa) |
pnpm format |
biome format --write . |
Reformat frontend files |
pnpm release |
node scripts/release.mjs |
Cut a release (see Releases and Auto-Updates) |
pnpm prepare |
husky |
Installs git hooks on pnpm install
|
flowchart LR
root["repo root"] --> src["src/ (React + TypeScript)"]
root --> st["src-tauri/ (Rust workspace)"]
root --> sc["scripts/ (Node tooling)"]
root --> gh[".github/workflows"]
root --> dc["docs/"]
root --> sv["server/ (room server, separate repo, git-ignored)"]
st --> app["src/ (app crate: proxy_builder_lib)"]
st --> core["crates/proxy-core (pure crate)"]
st --> mesa["crates/mesa (pure game engine)"]
st --> as["assets/ (house frame pack, fonts, JSON manifests)"]
| Path | Responsibility |
|---|---|
src/features/<area>/ |
One folder per UI area: decklist, preview, export, variants, card-editor, library, stats, set-browser, settings, shell, disenar, tutorial, mesa (game table), salas (rooms and playgroups) (see Frontend Architecture) |
src/stores/ |
Zustand stores: deck-store, plan-store, ui-store, trabajo-store, actualizacion-store, edicion-store, originales-store, and for the game table and online play mesa-store, vivo-store, chat-store, repeticion-store, red-store, salas-store (see State Stores) |
src/lib/ |
ipc.ts (the only door to Rust), trabajos.ts, image-url.ts, dialogos.ts, errores.ts, themes helpers |
src/components/ |
Shared components and the ui/ primitives (shadcn-style, Radix) |
src/i18n/ |
Interface dictionaries, one folder per language code with one file per area, plus the translator and the lazy loader (see Internationalization) |
src/bindings/ |
Generated TypeScript types from Rust (do not edit; excluded from Biome and Knip) |
src/themes.css, src/index.css, src/styles/, src/fonts.css
|
Theme tokens and global CSS (excluded from Biome) |
src-tauri/src/ |
App crate: lib.rs (bootstrap + command registration only), commands/<area>.rs, store/, scryfall/, fuentes/, diseno/, mesa/ (game table app layer), red/ (online play: iroh node, protocol, identity, profiles, room server client), enlace.rs (proxybuilder:// links), ocr.rs, actualizar.rs, temas.rs, error.rs, state.rs, jobs.rs, protocol.rs, registro.rs (see Rust App Layer) |
src-tauri/crates/proxy-core/ |
Pure core: decklist, layout, imaging, pdf, stats, diseno, model.rs, paper.rs, units.rs. No Tauri, no network, no database. Has the golden tests in tests/
|
src-tauri/crates/mesa/ |
Pure game engine: the game, its actions, randomness and what each player sees. No Tauri, no network. Tests in tests/ (see Testing and CI) |
server/ |
The room server, a separate repository cloned here and ignored by the app's git (see below) |
src-tauri/assets/ |
Bundled assets and manifests, e.g. letras-libres.json, packs-descargables.json, letras-cjk.json
|
src-tauri/capabilities/default.json |
Tauri permissions of the main window (including core:window:allow-destroy, needed by the close guard); no fs permission, all disk access goes through Rust commands |
scripts/ |
idiomas.mjs, sin-title-nativo.mjs, release.mjs, release-notas.mjs, tipos-impresos.mjs (regenerates crates/proxy-core/assets/tipos-impresos.json from Scryfall, the printed type lines per language; run by hand when a set adds types, needs network and takes about ten minutes per language because of Scryfall's rate limit), emoji-noto.mjs (copies the Noto Color Emoji images the UI uses into src/assets/emoji/; run by hand, needs network or --desde <clone>, and inkscape on the PATH only when an SVG is over 64 KB and must be converted to PNG), __tests__/, icono.py and reverso.py (generate the app icon and the generic card back with Pillow; run by hand) |
docs/ |
packs-de-marcos.md (frame-pack format) and presentacion/ (README screenshots; en/ for the English ones) |
.github/workflows/ |
ci.yml, release.yml
|
src-tauri/Cargo.toml defines the workspace (members = [".", "crates/proxy-core", "crates/mesa"]) and the profiles. The app library is named proxy_builder_lib (the _lib suffix avoids a name clash with the binary on Windows).
Types that cross the IPC boundary are Rust structs/enums annotated #[derive(TS)] #[ts(export)]. ts-rs generates a test per exported type (named export_bindings_<Type>); running those tests writes the .ts files.
flowchart LR
R["Rust type with derive TS and ts(export)"] -->|"pnpm bindings (cargo test ... export_bindings)"| B["src/bindings/Type.ts"]
B --> I["src/lib/ipc.ts (Commands interface)"]
I --> S["stores / components via call()"]
- The destination comes from
.cargo/config.tomlat the repo root:TS_RS_EXPORT_DIR = { value = "src/bindings", relative = true }. It lives at the root, not insrc-tauri/.cargo/, because Cargo reads config relative to the directory it is launched from and the pnpm scripts launch it from the root; insrc-tauri/.cargo/the types ended up insrc-tauri/bindings/. -
pnpm bindingsmust be re-run after changing any exported Rust type; commit the result.pnpm test:rustalso runs those export tests (so they rewrite the files as a side effect), but nothing in CI diffs the result against what is committed. Inferred: a stale committed binding is only caught indirectly, whentscinpnpm checkfails against it. -
src/bindings/is never edited by hand and is ignored by Biome and Knip. -
src/lib/ipc.tsthen declares every command in theCommandsinterface (argsandresulttypes) and every event inEvents. See IPC and Error Handling.
- WebKitGTK is the webview on Linux; scroll and animation behaviour must be verified there, not only in Chrome.
- On a machine without a GPU, start the app under a virtual display:
Xvfb :99and run withWEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1. Without these variables the entrance animations of dialogs do not advance in a GPU-less display, so dialogs stay invisible yet open and swallow clicks. Real desktops do not need them. - The window starts hidden with a dark background (
tauri.conf.json:visible: false,backgroundColor); the app shows it when the page has loaded (on_page_loadinsrc-tauri/src/lib.rs). The window-state plugin does not persist visibility (StateFlags::all() & !StateFlags::VISIBLE). - Updates only work in an installed build:
soportada()insrc-tauri/src/actualizar.rsis false underdebug_assertions(so never inpnpm tauri dev).
Debug builds read three variables; a release build ignores them (each check is behind cfg!(debug_assertions)):
| Variable | Read in | Effect |
|---|---|---|
PB_PERFIL=<name> |
src-tauri/src/red/perfil.rs, lib.rs
|
A separate instance on the same machine. The name must be letters, digits, - or _, up to 32 characters, and is lowercased; anything else is ignored. The profile gets its own data, cache and log folders (perfiles/<name> inside each app folder), so its own library and iroh identity (identidad/clave lives in the data folder); its own webview data folder (webview in its cache, so its own localStorage: language, preferences, tutorials seen); the profile in the window title; and no window-state plugin (the instances would overwrite each other's position). The single-instance plugin is also off with a profile, so several windows can be open at once; a proxybuilder:// link then is not handed to a running copy. Copy library.db* and blobs/ into the profile's data folder to have decks there. |
PB_RELAY |
src-tauri/src/red/nodo.rs (relay_de_entorno) |
none (case-insensitive): no relays, only direct addresses (LAN and tests). A relay URL: only that relay (to try your own). Any other value logs a warning and falls back to the default. Without it: the production setting, which for now is n0's public relays (RELE_PROPIO is None until there is a domain; once set, the own relay and n0's behind it). |
PB_SERVIDOR |
src-tauri/src/red/servidor/mod.rs (url) |
The room server URL, e.g. ws://127.0.0.1:8787/v1/ws; empty counts as unset. Without it the production URL is used, which is None for now, so without PB_SERVIDOR the rooms screen says there is no server. |
Other PB_* variables are for tests only and are listed in Testing and CI, except PB_PACKS_LOCAL (debug builds load downloadable frame packs from a local folder, see Frame Packs).
The room server is a separate repository cloned at server/ (ignored by the app's git; Node 22.13 or later):
cd server
pnpm install
pnpm dev # tsx watch src/index.ts; GET /healthz answers okIt reads its own variables (server/src/index.ts): PB_PUERTO (port, default 8787), PB_ESCUCHA (listen address, default 127.0.0.1, nginx goes in front in a deployment), PB_DB (SQLite file, default datos/servidor.db; :memory: for tests) and PB_RELAY_TOKEN (the token iroh-relay sends in its callout). Its other scripts: pnpm check (tsc, vitest, biome), pnpm test, pnpm protocolo:comprobar (the contract test only), pnpm build and pnpm start. With server/node_modules present, the app's pnpm test:rust also starts the server for a real test; see Testing and CI.
A typical online test on one machine: the server running as above, the main instance with PB_SERVIDOR=ws://127.0.0.1:8787/v1/ws pnpm tauri dev (add PB_RELAY=none to stay on local addresses), and more instances started from the debug binary with the same variables plus a profile, e.g. PB_PERFIL=b src-tauri/target/debug/proxy-builder and PB_PERFIL=c …. On a headless machine run them all on the same Xvfb display with the WebKit variables above. Inferred: the extra instances load the frontend from Vite's dev server (devUrl, port 1420), so pnpm tauri dev (or pnpm dev) has to be running for them to show anything. See Online Play.
pnpm install runs husky (prepare script) which wires .husky/pre-commit:
-
pnpm exec lint-stagedusinglint-staged.config.mjs. It fixes only staged files:-
*.{ts,tsx,js,jsx,mjs,cjs}runbiome check --write --no-errors-on-unmatched; -
.json/.html(except the pnpm lockfile) runbiome format --write; -
src-tauri/**/*.rsrunrustfmt --edition 2021.
-
- If any
.ts/.tsxfile is staged:tsc --noEmit,pnpm idiomas,pnpm check:title. - If any
.rsfile is staged andcargois on thePATH:pnpm check:clippy(with-D warnings). The hook prepends~/.cargo/bintoPATHbecause GUI git clients do not inherit the interactive shell'sPATH.
Gotchas:
- Use bare
rustfmt --edition 2021on staged files, nevercargo fmtin lint-staged:cargo fmtreformats the whole crate and would drag untouched files into the commit. The--edition 2021flag is mandatory: without it rustfmt assumes edition 2015 and silently leaves the file unchanged. - A new clippy warning is fixed, or allowed locally with
#[allow(clippy::...)]plus a comment explaining why;--no-verifyis not the answer. - The hook is deliberately cheap (the slow work,
vitest,knip,cargo test, is left topnpm checkand CI).
| Scope | Rule | Where |
|---|---|---|
| TS/TSX/JS/JSON/CSS/HTML |
Tabs (indent width 2, line width 100), double quotes, semicolons, trailing commas all (JSON: none), arrow parens always |
biome.json, .editorconfig
|
| Rust | Four spaces (rustfmt default) |
.editorconfig [*.rs]
|
| Markdown/YAML | Two spaces | .editorconfig |
| Biome rules |
recommended plus a list of explicit relaxations (e.g. noNonNullAssertion, useExhaustiveDependencies, many a11y rules off) |
biome.json |
| Biome ignores |
node_modules, dist, dist-ssr, tmp, src-tauri/target, src-tauri/gen, pnpm-lock.yaml, public, src/bindings, saved test data (src-tauri/crates/proxy-core/tests/compatibilidad, src-tauri/crates/mesa/tests/catalogo, src/stores/__tests__/fixtures), src/index.css, src/themes.css, src/fonts.css, src/styles, and a couple of local tool folders; plus everything in .gitignore (vcs.useIgnoreFile), which is how server/ stays out |
biome.json files.includes, vcs
|
| Rust lints | Clippy with -D warnings; #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] in src-tauri/src/lib.rs, crates/proxy-core/src/lib.rs and crates/mesa/src/lib.rs
|
Tests may use unwrap; test files add #![allow(clippy::unwrap_used, clippy::expect_used)]
|
| TypeScript |
strict, noUnusedLocals, noUnusedParameters, alias @/* to src/*
|
tsconfig.json |
Rust dev profile specifics (src-tauri/Cargo.toml): debug = "line-tables-only" with split-debuginfo = "packed" to keep target/ small (unpacked debuginfo once grew a sibling project's target past 100 GB; run cargo clean after touching it), and dependencies are compiled with opt-level = 2 even in dev because image processing at 600 PPP unoptimised takes seconds and would make the live preview unusable under tauri dev. The mesa crate gets opt-level = 2 in dev too: every action copies and compares the whole game, and its property tests play thousands.
| Symptom | Check |
|---|---|
pnpm install rejects the lockfile |
Regenerate with the packageManager pnpm version; see policy above |
| Port 1420 busy |
strictPort makes Vite fail; free the port |
Types in src/bindings stale or missing |
pnpm bindings; confirm .cargo/config.toml is at the repo root |
cargo cannot find GTK/WebKit |
Install the Linux packages above |
| Dialogs invisible on a headless display | WebKit env variables above |
| Rooms say there is no server | Debug build with PB_SERVIDOR set and the server running (see above) |
| Log of what the app did |
proxy-builder.log in the app log dir (src-tauri/src/registro.rs) |
The maintainer reviews changes in their own working tree: automated sessions leave edits uncommitted and do not push (a push triggers CI and failure emails). Before handing a patch over run the same gates as CI: pnpm bindings, pnpm check, pnpm check:rust-fmt, pnpm check:clippy (latest stable toolchain), pnpm test:rust, with biome check --write and cargo fmt applied first (lint-staged would do it at commit time). The project also expects the code-graph index to be refreshed at the end of each phase of work.
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia