Skip to content

Latest commit

 

History

History
162 lines (133 loc) · 8.25 KB

File metadata and controls

162 lines (133 loc) · 8.25 KB

tauri-typegen

tauri-typegen generates TypeScript (and optionally Zod) bindings from a Tauri backend's Rust code. Tauri ships no built-in way to keep frontend types in sync with #[tauri::command] functions and their models; this crate fills that gap by parsing the Rust source with syn and rendering typed output with Tera templates.

It ships as both a library (tauri-typegen) and a cargo subcommand binary (cargo-tauri-typegen). Consumers use it as a build dependency (BuildSystem::generate_at_build_time() in build.rs) or as a CLI (cargo tauri-typegen generate).

  • Edition: 2021 · MSRV: 1.77.2 · CI toolchain: 1.93.0
  • Validation modes: none (vanilla TS types) and zod (runtime schemas + hooks). There are no other modes — don't introduce one without instruction.

Build, test, and verify

Run these from the repo root. They mirror CI (.github/workflows/ci.yml); all three must pass before a change is complete:

cargo test --all-features
cargo fmt -- --check          # formatting is enforced; run `cargo fmt` to fix
cargo clippy --all-targets --all-features -- -D warnings   # warnings fail CI

To run a focused test: cargo test <name> (e.g. cargo test command_parser, cargo test --test integration_e2e <name>).

Architecture (the generation pipeline)

The flow is analysis → models → generation, single-pass over a cached AST.

  1. Analysis (src/analysis/) — parses Rust files into the data model.
    • ast_cache.rs parses and caches every .rs file once (single-pass goal).
    • command_parser.rs, struct_parser.rs, event_parser.rs, channel_parser.rs, serde_parser.rs, validator_parser.rs extract their respective constructs.
    • type_resolver.rs + dependency_graph.rs perform lazy, on-demand type discovery. mod.rs (CommandAnalyzer) orchestrates the pass.
  2. Models (src/models.rs) — the shared data model: CommandInfo, ParameterInfo, StructInfo, FieldInfo, EnumVariantInfo, TypeStructure, etc. Analysis fills these; generation reads them.
  3. Generation (src/generators/) — turns models into output files.
    • base/ holds shared machinery: template_context.rs (the *Context structs handed to templates), type_visitor.rs (the TypeVisitor trait), and templates.rs.
    • ts/ and zod/ each provide a generator, a TypeVisitor impl, and a templates/ directory of .tera files. Zod adds schema_builder.rs and filters.rs.
    • mod.rs builds the contexts (create_command_contexts, create_event_contexts, …) consumed by every template.
  4. Build/IO (src/build/) — project scanning, caching (.typecache), and writing output. Interface (src/interface/) — CLI, config parsing, logging. BuildSystem and GenerateConfig are re-exported from lib.rs.

Conventions you must follow

  • Match surrounding code. This is idiomatic Rust formatted with rustfmt; keep clippy clean (CI denies warnings).
  • Deduplication happens at the generation layer, not analysis. Commands and events are deduped by name (first-occurrence-wins) in src/generators/mod.rs (create_command_contexts / create_event_contexts). The analyzer stays a faithful record of the source — do not dedupe there. This is how cross-platform #[cfg(...)]-gated duplicate commands collapse to one declaration.
  • Parse serde/attribute metadata with attr.parse_nested_meta(...), the AST-based approach (see serde_parser.rs), not string matching.
  • Serde naming precedence: field/parameter-level #[serde(rename = "...")] overrides container-level #[serde(rename_all = "...")]. Command-level rename_all affects parameters/channels only — never the command (invoke) name or the generated function name.
  • Enums: all-unit-variant enums become string-literal unions / z.enum; enums with data become discriminated unions / z.discriminatedUnion. See is_simple_enum() / is_complex_enum() on StructInfo.
  • Output formatting lives in templates. Change .tera files under src/generators/{ts,zod}/templates/ for output shape; keep the two backends consistent. Both commands.ts functions and the types.ts Params interfaces derive from the same deduped contexts — don't reintroduce a second source.

Adding or changing features

  • New tests are required. Per repo policy: changes are validated against existing tests; new features are validated against new tests.
    • Unit tests: #[cfg(test)] modules colocated in the relevant src/** file.
    • End-to-end tests: tests/integration_e2e.rs, using the TestProject / TestGenerator helpers in tests/common/mod.rs and fixtures in tests/fixtures/. Prefer an e2e test when behavior spans analysis + generation (e.g. the cfg-dedup regression test).
    • Backward-compatibility tests: tests/regression/.
  • Touching the data model (models.rs) ripples into both generators and their templates — update all of analysis, both TypeVisitor impls, and the affected .tera files together.
  • Always run the full verify trio (test + fmt + clippy) before declaring done.

Versioning and changelog

The project follows Semantic Versioning and keeps a Keep a Changelog-style CHANGELOG.md. Git tags are bare version numbers, no v prefix (e.g. 0.5.1).

The version source of truth is crates.io, not Cargo.toml. The version in Cargo.toml is intentionally stale and is never bumped for a release — do not "update the version" there as part of a change. The release workflow (.github/workflows/cd.yml) reads the latest published version from crates.io, computes the next one, and overwrites Cargo.toml only on the CI runner (the change is not committed back).

Releases are manual via the CD workflow's workflow_dispatch trigger with a major / minor / patch choice. The workflow then: fetches the current crates.io version → bumps it → extracts release notes from CHANGELOG.md for the new version → cargo set-version → build + test → cargo publish → gh release create (tag + GitHub release).

What this means for you as an agent:

  • When a change is user-facing, add or update a CHANGELOG entry for the upcoming version. CD fails if no ## [<new_version>] - <date> section exists for the version being released, so the entry must already be in place (committed via PR) before a release is run.
  • Use the existing section structure: ## [X.Y.Z] - YYYY-MM-DD with ### Added / ### Changed / ### Fixed subsections, newest at the top. Match the entry style — a bold lead-in followed by sub-bullets.
  • Pick the next version per SemVer relative to the latest crates.io release (which is what CD will bump from), not the number in Cargo.toml.
  • Do not create git tags, run cargo publish, or bump Cargo.toml yourself — releasing is the CD workflow's job.

Roadmap (maintainer notes — not active work unless asked)

Future plans

  • When using zod, schema validation should match the parameters in the validator macro. Example: #[validate(range(min = 1, max = 10, message = "Must be between 1 and 10"))] → z.coerce.number().min(1).max(10), carrying over the message per https://zod.dev/api. (Groundwork: validator_parser.rs.)

Nice to have — Macro-Based Command Discovery

Concept: extract commands from tauri::generate_handler! invocations instead of parsing all files for #[tauri::command].

Benefits: only analyzes registered commands; better performance; no false positives from unused commands; single source of truth.

Approaches: (1) parse main.rs/lib.rs for tauri::generate_handler![...]; (2) hook the build/macro-expansion process; (3) custom proc-macro wrapper around generate_handler!; (4) hybrid — macro parsing first, fall back to file parsing, cross-reference to validate.

Challenges: multiple/conditional registration points (plugins), complex proc-macro analysis, compilation-process integration, dynamic registration.

Resources:

Priority: Low — current file-parsing approach works well for most cases.