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 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-devCI (.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).
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 |
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/"]
st --> app["src/ (app crate: proxy_builder_lib)"]
st --> core["crates/proxy-core (pure crate)"]
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 (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).
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).
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 |
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.
| 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) |
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
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