An open API service indexing awesome lists of open source software.

https://github.com/thwbh/tauri-typegen

A rust crate that automatically generates TypeScript models and bindings from your Tauri commands
https://github.com/thwbh/tauri-typegen

bindings bindings-generator codegen command-line-tool tauri tauri-plugin tauri2 typescript-bindings zod-validation

Last synced: 4 months ago
JSON representation

A rust crate that automatically generates TypeScript models and bindings from your Tauri commands

Awesome Lists containing this project

README

          

# Tauri TypeGen

[![Crates.io](https://img.shields.io/crates/v/tauri-typegen.svg)](https://crates.io/crates/tauri-typegen)
[![Documentation](https://docs.rs/tauri-typegen/badge.svg)](https://docs.rs/tauri-typegen)
[![codecov](https://codecov.io/gh/thwbh/tauri-typegen/branch/main/graph/badge.svg)](https://codecov.io/gh/thwbh/tauri-typegen)
[![Test](https://github.com/thwbh/tauri-typegen/actions/workflows/ci.yml/badge.svg)](https://github.com/thwbh/tauri-typegen/actions/workflows/ci.yml)

A command-line tool that automatically generates TypeScript bindings from your Tauri commands, eliminating manual frontend type creation.

## Features

- 🔍 **Automatic Discovery**: Scans Rust source for `#[tauri::command]` functions
- 📝 **TypeScript Generation**: Creates TypeScript interfaces for command parameters and return types
- ✅ **Validation Support**: Optional Zod schema generation with runtime validation
- 🚀 **Command Bindings**: Strongly-typed frontend functions
- 📡 **Event Support**: Discovers and types `app.emit()` events
- 📞 **Channel Support**: Types for streaming `Channel` parameters
- 🏷️ **Serde Support**: Respects `#[serde(rename)]` and `#[serde(rename_all)]` attributes
- 🎯 **Type Safety**: Keeps frontend and backend types in sync
- 🛠️ **Build Integration**: Works as standalone CLI or build dependency
- ⚡ **Smart Caching**: Only regenerates when source files change

## Table of Contents

- [Installation](#installation)
- [Quick Setup](#quick-setup)
- [Recommended Setup](#recommended-setup)
- [Generated Code](#generated-code)
- [Using Generated Bindings](#using-generated-bindings)
- [TypeScript Compatibility](#typescript-compatibility)
- [API Reference](#api-reference)
- [Configuration](#configuration)
- [Caching](#caching)
- [Usage in CI](#usage-in-ci)
- [Examples](#examples)
- [Contributing](#contributing)

## Installation

Install globally as a CLI tool:

```bash
cargo install tauri-typegen
```

**Or** add as a build dependency to your Tauri project:

```bash
cargo add --build tauri-typegen
```

## Quick Setup

For trying it out or one-time generation:

```bash
# Install CLI
cargo install tauri-typegen

# Generate types once
cargo tauri-typegen generate

# Use generated bindings
```

This generates TypeScript files in `./src/generated/` from your `./src-tauri/` code.

## Recommended Setup

For integrated development workflow:

### 1. Install and Initialize

```bash
# Install CLI
cargo install tauri-typegen

# Initialize configuration (adds to tauri.conf.json)
cargo tauri-typegen init

# Or with custom settings
cargo tauri-typegen init --validation zod --output tauri.conf.json
```

This creates a configuration block in your `tauri.conf.json`:

```json
{
"plugins": {
"tauri-typegen": {
"project_path": ".",
"output_path": "../src/generated",
"validation_library": "none",
"verbose": false
}
}
}
```

### 2. Add Build Hook

Add `tauri-typegen` as a build dependency from within your Tauri project (in the `src-tauri` directory):

```bash
cd src-tauri
cargo add --build tauri-typegen
cd ..
```

Then add to `src-tauri/build.rs`:

```rust
fn main() {
// Generate TypeScript bindings before build
tauri_typegen::BuildSystem::generate_at_build_time()
.expect("Failed to generate TypeScript bindings");

tauri_build::build()
}
```

Now types auto-generate on every Rust build:

```bash
npm run tauri dev # Types generated automatically
npm run tauri build # Types generated automatically
```

## Generated Code

### Example Rust Code

```rust
use serde::{Deserialize, Serialize};
use tauri::ipc::Channel;

#[derive(Serialize, Deserialize)]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
}

#[derive(Deserialize)]
pub struct CreateUserRequest {
pub name: String,
pub email: String,
}

#[derive(Clone, Serialize)]
pub struct ProgressUpdate {
pub percentage: f32,
pub message: String,
}

// Simple command
#[tauri::command]
pub async fn get_user(id: i32) -> Result {
// Implementation
}

// Command with custom types
#[tauri::command]
pub async fn create_user(request: CreateUserRequest) -> Result {
// Implementation
}

// Command with Channel for progress streaming
#[tauri::command]
pub async fn download_file(
url: String,
on_progress: Channel
) -> Result {
// Send progress updates
on_progress.send(ProgressUpdate {
percentage: 50.0,
message: "Halfway done".to_string()
})?;
// Implementation
}

// Event emission
pub fn notify_user(app: &AppHandle, message: String) {
app.emit("user-notification", message).unwrap();
}
```

### Generated Files

```
src/generated/
├── types.ts # TypeScript interfaces
├── commands.ts # Typed command functions
└── events.ts # Event listener functions (if events detected)
```

**Generated `types.ts`:**

```typescript
import type { Channel } from '@tauri-apps/api/core';

export interface User {
id: number;
name: string;
email: string;
}

export interface CreateUserRequest {
name: string;
email: string;
}

export interface ProgressUpdate {
percentage: number;
message: string;
}

export interface GetUserParams {
id: number;
}

export interface CreateUserParams {
request: CreateUserRequest;
}

export interface DownloadFileParams {
url: string;
onProgress: Channel;
}
```

**Generated `commands.ts`:**

```typescript
import { invoke, Channel } from '@tauri-apps/api/core';
import * as types from './types';

export async function getUser(params: types.GetUserParams): Promise {
return invoke('get_user', params);
}

export async function createUser(params: types.CreateUserParams): Promise {
return invoke('create_user', params);
}

export async function downloadFile(params: types.DownloadFileParams): Promise {
return invoke('download_file', params);
}
```

**Generated `events.ts`:**

```typescript
import { listen } from '@tauri-apps/api/event';

export async function onUserNotification(handler: (event: string) => void) {
return listen('user-notification', (event) => handler(event.payload as string));
}
```

### With Zod Validation

When using `--validation zod`, generated commands include runtime validation:

```typescript
export async function createUser(
params: types.CreateUserParams,
hooks?: CommandHooks
): Promise {
try {
const result = types.CreateUserParamsSchema.safeParse(params);

if (!result.success) {
hooks?.onValidationError?.(result.error);
throw result.error;
}

const data = await invoke('create_user', result.data);
hooks?.onSuccess?.(data);
return data;
} catch (error) {
if (!(error instanceof ZodError)) {
hooks?.onInvokeError?.(error);
}
throw error;
} finally {
hooks?.onSettled?.();
}
}
```

## Using Generated Bindings

### Basic Usage

```typescript
import { getUser, createUser, downloadFile } from './generated';
import { Channel } from '@tauri-apps/api/core';

// Simple command
const user = await getUser({ id: 1 });

// With custom types
const newUser = await createUser({
request: {
name: "John Doe",
email: "john@example.com"
}
});

// With Channel for streaming
const onProgress = new Channel();
onProgress.onmessage = (progress) => {
console.log(`${progress.percentage}%: ${progress.message}`);
};

const result = await downloadFile({
url: "https://example.com/file.zip",
onProgress
});
```

### With Event Listeners

```typescript
import { onUserNotification } from './generated';

// Listen for events
const unlisten = await onUserNotification((message) => {
console.log('Notification:', message);
});

// Stop listening
unlisten();
```

### React Example

```tsx
import React, { useState } from 'react';
import { createUser } from './generated';
import type { User } from './generated';

export function CreateUserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

const user = await createUser({
request: { name, email }
});

console.log('Created:', user);
};

return (

setName(e.target.value)} />
setEmail(e.target.value)} />
Create User

);
}
```

### With Zod Validation Hooks

```typescript
import { createUser } from './generated';
import { toast } from 'sonner';

await createUser(
{ request: userData },
{
onValidationError: (err) => toast.error(err.errors[0].message),
onInvokeError: (err) => toast.error('Failed to create user'),
onSuccess: (user) => toast.success(`Created ${user.name}!`),
}
);
```

## TypeScript Compatibility

### Requirements

- **TypeScript 5.0+**
- **Zod 4.x** (when using Zod validation)
- **ES2018+** target

### tsconfig.json

```json
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
```

### Type Mappings

| Rust Type | TypeScript |
|-----------|-----------|
| `String`, `&str` | `string` |
| `i8`, `i16`, `i32`, `i64`, `i128`, `isize` | `number` |
| `u8`, `u16`, `u32`, `u64`, `u128`, `usize` | `number` |
| `f32`, `f64` | `number` |
| `bool` | `boolean` |
| `()` | `void` |
| `Option` | `T \| null` |
| `Vec` | `T[]` |
| `HashMap`, `BTreeMap` | `Record` |
| `HashSet`, `BTreeSet` | `T[]` |
| `(T, U, V)` | `[T, U, V]` |
| `Channel` | `Channel` |
| `Result` | `T` (errors via Promise rejection) |

### Serde Attribute Support

Tauri-typegen respects serde serialization attributes to ensure generated TypeScript types match your JSON API:

#### Field Renaming

```rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct User {
#[serde(rename = "userId")]
pub user_id: i32,
pub name: String,
}
```

Generates:

```typescript
export interface User {
userId: number; // Field renamed as specified
name: string;
}
```

#### Struct-Level Naming Convention

```rust
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiResponse {
pub user_id: i32,
pub user_name: String,
pub is_active: bool,
}
```

Generates:

```typescript
export interface ApiResponse {
userId: number; // snake_case → camelCase
userName: string; // snake_case → camelCase
isActive: boolean; // snake_case → camelCase
}
```

**Supported naming conventions:**
- `camelCase`
- `PascalCase`
- `snake_case`
- `SCREAMING_SNAKE_CASE`
- `kebab-case`
- `SCREAMING-KEBAB-CASE`

#### Field Precedence

Field-level `rename` takes precedence over struct-level `rename_all`:

```rust
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub user_id: i32, // → userId
#[serde(rename = "fullName")]
pub user_name: String, // → fullName (override)
}
```

#### Skip Fields

```rust
#[derive(Serialize, Deserialize)]
pub struct User {
pub id: i32,
#[serde(skip)]
pub internal_data: String, // Not included in TypeScript
}
```

#### Enum Support

Enums also support serde rename attributes:

```rust
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MyEnum {
HelloWorld, // → HELLO_WORLD
ByeWorld, // → BYE_WORLD
}
```

Generates:

```typescript
export type MyEnum = "HELLO_WORLD" | "BYE_WORLD";

// With Zod:
export const MyEnumSchema = z.enum(["HELLO_WORLD", "BYE_WORLD"]);
```

Variant-level rename also works:

```rust
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Status {
InProgress, // → inProgress
#[serde(rename = "not-started")]
NotStarted, // → not-started (override)
}
```

## API Reference

### CLI Commands

```bash
# Generate bindings
cargo tauri-typegen generate [OPTIONS]

Options:
-p, --project-path Tauri source directory [default: ./src-tauri]
-o, --output-path Output directory [default: ./src/generated]
-v, --validation Validation library: zod or none [default: none]
--verbose Verbose output
--visualize-deps Generate dependency graph
-c, --config Config file path
-f, --force Force regeneration, ignoring cache
```

```bash
# Initialize configuration
cargo tauri-typegen init [OPTIONS]

Options:
-p, --project-path Tauri source directory [default: ./src-tauri]
-g, --generated-path Output directory [default: ./src/generated]
-o, --output Config file [default: tauri.conf.json]
-v, --validation Validation library [default: none]
--force Overwrite existing config
```

### Build Script API

Add as a build dependency:

```bash
cd src-tauri
cargo add --build tauri-typegen
```

Then in `src-tauri/build.rs`:

```rust
fn main() {
// Generate TypeScript bindings
tauri_typegen::BuildSystem::generate_at_build_time()
.expect("Failed to generate TypeScript bindings");

tauri_build::build()
}
```

### Programmatic API

```rust
use tauri_typegen::{GenerateConfig, generate_from_config};

let config = GenerateConfig {
project_path: ".".to_string(),
output_path: "../src/generated".to_string(),
validation_library: "none".to_string(),
verbose: Some(true),
};

let files = generate_from_config(&config)?;
```

## Configuration

### Standalone Config File

```json
{
"project_path": ".",
"output_path": "../src/generated",
"validation_library": "none",
"verbose": false
}
```

### Tauri Config Integration

In `tauri.conf.json`:

```json
{
"plugins": {
"typegen": {
"projectPath": ".",
"outputPath": "../src/generated",
"validationLibrary": "zod",
"verbose": true,
"force": false
}
}
}
```

### Validation Options

- **`none`** (default): TypeScript types only, no runtime validation
- **`zod`**: Generate Zod schemas with runtime validation and hooks

### Custom Type Mappings

Map external Rust types to TypeScript types for libraries like `chrono`, `uuid`, or custom types:

```json
{
"plugins": {
"tauri-typegen": {
"project_path": ".",
"output_path": "../src/generated",
"validation_library": "zod",
"type_mappings": {
"DateTime": "string",
"PathBuf": "string",
"Uuid": "string"
}
}
}
}
```

**Use cases:**
- External crate types: `chrono::DateTime` → `string`
- Standard library types: `std::path::PathBuf` → `string`
- Third-party types: `uuid::Uuid` → `string`
- Custom wrapper types: `UserId` → `number`

**Example:**

Rust code:
```rust
use chrono::{DateTime, Utc};
use std::path::PathBuf;

#[derive(Serialize)]
pub struct FileMetadata {
pub path: PathBuf,
pub created_at: DateTime,
}

#[tauri::command]
pub fn get_file_info() -> FileMetadata {
// ...
}
```

Generated TypeScript (with mappings):
```typescript
export interface FileMetadata {
path: string; // PathBuf → string
createdAt: string; // DateTime → string
}

export async function getFileInfo(): Promise {
return invoke('get_file_info');
}
```

## Caching

Tauri-typegen uses smart caching to skip regeneration when nothing has changed, improving build times.

### How It Works

A `.typecache` file is created in your output directory containing hashes of:
- All discovered Tauri commands
- All discovered structs and enums
- Configuration settings that affect output

On subsequent runs, these hashes are compared. If nothing changed, generation is skipped.

### Force Regeneration

To bypass the cache and force regeneration:

**CLI flag (highest priority):**
```bash
cargo tauri-typegen generate --force
# or
cargo tauri-typegen generate -f
```

**Config file (`tauri.conf.json`):**
```json
{
"plugins": {
"typegen": {
"force": true
}
}
}
```

**Programmatic:**
```rust
let mut config = GenerateConfig::default();
config.force = Some(true);
```

The CLI `--force` flag always overrides the config file value.

### Cache File Location

The cache file `.typecache` is stored in your output directory (e.g., `./src/generated/.typecache`). Add it to `.gitignore`:

```gitignore
# Tauri-typegen cache
.typecache
```

Or if your entire output directory is gitignored, the cache file is already excluded.

## Usage in CI

When running builds in CI/CD environments, you need to generate TypeScript bindings before the frontend build step.

### Why CI Needs Special Setup

The `cargo tauri build` command builds the frontend bundle first, before compiling Rust code. This means the build script in `src-tauri/build.rs` hasn't run yet, so bindings aren't generated when the frontend needs them.

### Recommended CI Workflow

Install and run the CLI tool as a separate step before building:

```yaml
# GitHub Actions example
- name: Install tauri-typegen
run: cargo install tauri-typegen

- name: Generate TypeScript bindings
run: cargo tauri-typegen generate

- name: Build Tauri app
run: npm run tauri build
```

```yaml
# GitLab CI example
build:
script:
- cargo install tauri-typegen
- cargo tauri-typegen generate
- npm run tauri build
```

### Alternative: Cache the CLI Installation

To speed up CI runs, cache the installed binary:

```yaml
# GitHub Actions with caching
- name: Cache tauri-typegen
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cargo-tauri-typegen
key: ${{ runner.os }}-tauri-typegen-${{ hashFiles('**/Cargo.lock') }}

- name: Install tauri-typegen
run: cargo install tauri-typegen --locked

- name: Generate bindings
run: cargo tauri-typegen generate

- name: Build
run: npm run tauri build
```

## Examples

See the examples repository: https://github.com/thwbh/tauri-typegen-examples

## Contributing

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request

## License

This project is licensed under the MIT license.