Skip to content

Development Setup

Pablo Martínez López edited this page Oct 23, 2026 · 6 revisions

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.

Prerequisites

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 and crates/proxy-core/Cargo.toml) CI uses the latest stable with clippy and rustfmt
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-dev

CI (.github/workflows/ci.yml) installs libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf on ubuntu-22.04. The app crate links GTK/WebKit, so cargo test/clippy for the whole workspace need those libraries even though you never open a window; proxy-core alone (cargo test -p proxy-core) does 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).

Quick start

pnpm install
pnpm tauri dev

pnpm 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.

The pnpm policy

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.yaml with the pnpm version in packageManager. A lockfile produced by pnpm 10 skips the age rule and can pull brand-new versions.
  • CI installs with pnpm install --frozen-lockfile (both ci.yml and release.yml).
  • Biome ignores pnpm-lock.yaml and lint-staged excludes it from JSON formatting.

Scripts

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
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

Repository layout

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/"]
    st --> app["src/ (app crate: proxy_builder_lib)"]
    st --> core["crates/proxy-core (pure crate)"]
    st --> as["assets/ (house frame pack, fonts, JSON manifests)"]
Loading
Path Responsibility
src/features/<area>/ One folder per UI area: decklist, preview, export, variants, card-editor, library, stats, set-browser, settings, shell, disenar (see Frontend Architecture)
src/stores/ Zustand stores: deck-store, plan-store, ui-store, trabajo-store, actualizacion-store, edicion-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/, 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/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"]) and the profiles. The app library is named proxy_builder_lib (the _lib suffix avoids a name clash with the binary on Windows).

The bindings flow (ts-rs)

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()"]
Loading
  • The destination comes from .cargo/config.toml at the repo root: TS_RS_EXPORT_DIR = { value = "src/bindings", relative = true }. It lives at the root, not in src-tauri/.cargo/, because Cargo reads config relative to the directory it is launched from and the pnpm scripts launch it from the root; in src-tauri/.cargo/ the types ended up in src-tauri/bindings/.
  • pnpm bindings must be re-run after changing any exported Rust type; commit the result. pnpm test:rust also 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, when tsc in pnpm check fails against it.
  • src/bindings/ is never edited by hand and is ignored by Biome and Knip.
  • src/lib/ipc.ts then declares every command in the Commands interface (args and result types) and every event in Events. See IPC and Error Handling.

Running and testing the window in the cloud or headless

  • 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 :99 and run with WEBKIT_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_load in src-tauri/src/lib.rs). The window-state plugin does not persist visibility (StateFlags::all() & !StateFlags::VISIBLE).
  • Updates only work in an installed build: soportada() in src-tauri/src/actualizar.rs is false under debug_assertions (so never in pnpm tauri dev).

Git hooks and automatic fixes

pnpm install runs husky (prepare script) which wires .husky/pre-commit:

  1. pnpm exec lint-staged using lint-staged.config.mjs. It fixes only staged files:
    • *.{ts,tsx,js,jsx,mjs,cjs} run biome check --write --no-errors-on-unmatched;
    • .json/.html (except the pnpm lockfile) run biome format --write;
    • src-tauri/**/*.rs run rustfmt --edition 2021.
  2. If any .ts/.tsx file is staged: tsc --noEmit, pnpm idiomas, pnpm check:title.
  3. If any .rs file is staged and cargo is on the PATH: pnpm check:clippy (with -D warnings). The hook prepends ~/.cargo/bin to PATH because GUI git clients do not inherit the interactive shell's PATH.

Gotchas:

  • Use bare rustfmt --edition 2021 on staged files, never cargo fmt in lint-staged: cargo fmt reformats the whole crate and would drag untouched files into the commit. The --edition 2021 flag 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-verify is not the answer.
  • The hook is deliberately cheap (the slow work, vitest, knip, cargo test, is left to pnpm check and CI).

Formatting and lint rules

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 dist, tmp, src-tauri/target, src-tauri/gen, pnpm-lock.yaml, public, src/bindings, src/index.css, src/themes.css, src/fonts.css, src/styles biome.json files.includes
Rust lints Clippy with -D warnings; #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] in both src-tauri/src/lib.rs and crates/proxy-core/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.

Where to look when something does not start

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
Log of what the app did proxy-builder.log in the app log dir (src-tauri/src/registro.rs)

Automated contributors

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.

See also

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Frontend · Frontend

Reference · Referencia

Clone this wiki locally