tauri-apps / tauri

Build smaller, faster, and more secure desktop applications with a web frontend.
https://tauri.app
Apache License 2.0
81.73k stars 2.45k forks source link

Feature: Generate Typescript functions for Rust commands #1514

Open nganhkhoa opened 3 years ago

nganhkhoa commented 3 years ago

Goal: Atomatically generate Typescript functions for Rust commands in Typescript environment

Why: Use Typescript with type checking for Rust commands rather than invoke('some string', somedata)

How:

Assume these Rust commands are available

#[tauri::command]
async fn my_first_custom_command(message: String) -> Result<String, String> {
  Ok("simple type command".into())
}

#[derive(serde::Serialize)]
struct CustomResponse {
  message: String,
  other_val: usize,
}

#[derive(serde::Deserialize)]
struct CustomRequest {
  message: String,
  request: usize,
}

#[tauri::command]
async fn my_second_custom_command(req: CustomRequest, hello: String) -> Result<CustomResponse, String> {
  Ok(CustomResponse {
    message: "custom response".into(),
    other_val: 42,
  })
}

Using Typescript, we can create functions that conform to the Rust commands:

import tauriapi from "@tauri-apps/api";

const { invoke } = tauriapi.tauri;

function my_first_custom_command(message: String): Promise<String> {
  return invoke('my_first_custom_command', {
    message
  })
}

type CustomRequest = {
  message: String;
  request: Number;
}

type CustomResponse = {
  message: String;
  other_val: Number;
}

function my_second_custom_command(req: CustomRequest, hello: String): Promise<CustomResponse>{
  return invoke('my_second_custom_command', {req, hello})
}

export {
  my_first_custom_command,
  my_second_custom_command
}

Now we can call my_first_custom_command and my_second_custom_command with type checking in Typescript.

my_first_custom_command("this must be string")
.then((res) => {
  // res is String
  console.log(res);
});

my_second_custom_command({
  message: "this must be CustomRequest",
  request: 123,
}, "this must be string")
.then((res) => {
  // res is CustomResponse
  console.log(res.message, res.other_val);
});

The steps can be:

Problems that may arises:

Simple solution to the above solutions:

In discord chat, Denjell suggest that a WASM function can be generated from these Rust commands.

Originated from: https://discord.com/channels/616186924390023171/731495047962558564/832483720728412192

sagudev commented 2 years ago

I just want to mention Aleph-Alpha/ts-rs that does a great job on creating TS from Structs. Maybe something similar could be used in tauri.

async3619 commented 2 years ago

I think we could achive it with parse rust codes with AST (or something else) and generate TypeScript definition files like how graphql-code-generator works.

eric-burel commented 2 years ago

I think we could achive it with parse rust codes with AST (or something else) and generate TypeScript definition files like how graphql-code-generator works.

Just for the inspiration, maybe tRPC architecture is a good place to start: https://trpc.io/

Cobular commented 1 year ago

Any status update on this? Just curious where we stand rn

FabianLars commented 1 year ago

@Cobular The team itself is currently not working on this (otherwise there would be at least some info here). With the upcoming IPC changes for v2 this is further delayed, at least on our side, i still think (or hope) that this could be tackled outside of tauri 🤔

Cobular commented 1 year ago

I see! I’ll see if I have time! On Sep 26, 2022, 06:09 -0700, Fabian-Lars @.***>, wrote:

@Cobular The team itself is currently not working on this (otherwise there would be at least some info here). With the upcoming IPC changes for v2 this is further delayed, at least on our side, i still think (or hope) that this could be tackled outside of tauri 🤔 — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>

oscartbeaumont commented 1 year ago

@FabianLars refers to the potential for community work around this. I have been working on rspc for the last little bit. rspc is designed to be an alternative to tRPC that works with a Rust backend and Typescript frontend. It creates completely end-to-end typesafe APIs and has a plugin so it can easily be used with Tauri. It also has optional React and SolidJS hooks on the frontend which use Tanstack Query under the hood.

It is still very much a work-in-progress project and will likely have breaking changes in the future as I improve the API and introduce new features. Both Spacedrive (my employer) and Twidge have been using the rspc + Prisma Client Rust stack and it has been working pretty well.

If Tauri were ever going to tackle this upstream they could have a look at Specta which is the type exporting system @brendonovich and I made for rspc. Specta differs from ts-rs by its approach. Specta can take in a single type and recursively export all the types it depends on. With ts-rs there isn't an easy way to do that as they have designed the core around the assumption you will export each type individually with a dedicated derive macro. Specta also has a fairly language-agnostic core so it could potentially be used to export Rust types into various other languages although right now Typescript is the main focus. I have been messing around with supporting OpenAPI for example.

oscartbeaumont commented 1 year ago

@brendonovich and I were talking about this and got to hacking. We managed to get typesafe Tauri commands working on top of Specta. The repository shows an example application that can export the command types to Typescript (and OpenAPI with a few caveats). The code can be found here. It's still a bit of a tech demo but it could be turned into a publishable library or even officially adopted by Tauri if they were interested.

Cobular commented 1 year ago

There's another crate to do the generation for this now courtesy of 1password - typeshare - crates.io: Rust Package Registry. If anyone else gets wroking on this beyond Oscar and Brendon's thing, could be helpful!

JonasKruckenberg commented 1 year ago

Might be worth noting that I'm working on https://github.com/tauri-apps/tauri-bindgen now too, it has slightly different behavior and aims to the solutions brought up before, but it can generate typescript files for a given interface too so might be interesting

Cobular commented 1 year ago

That looks fantastic! This does indicate it's under heavy development, but how usable would you say this is today? I've just implemented build scripts to provide consistent types from my backend to the Tauri host so I'd I could add this to my project it'd be very helpful.

Brendonovich commented 1 year ago

@Cobular Typeshare is indeed interesting, but would it be applicable for Tauri commands given that they don't support function types? They also have no way to filter out Tauri-specific command arguments. I don't think static analysis would be powerful enough unless you used some sort of enum-based request and response model.

As an aside, I've been working the past couple of days to make Specta more powerful than both ts-rs and Typeshare, and capable of exporting types on its own. Will be interesting to see which solutions the community prefers!

JonasKruckenberg commented 1 year ago

That looks fantastic! This does indicate it's under heavy development, but how usable would you say this is today? I've just implemented build scripts to provide consistent types from my backend to the Tauri host so I'd I could add this to my project it'd be very helpful.

You can use it with two big asterisks:

  1. While the user facing API (i.e. the macros) are pretty stable the internals are absolutely not. And I can't guarantee some of that might leak through.
  2. The async option for the host produces code with lifetime errors atm, soo no async commands.

But: it would be tremendously helpful to have early testers, so by all means try it out please! I will be very responsive in the issues.

Just make sure to pin the crates to specific commits so your code doesn't unexpectedly break

WillsterJohnson commented 1 year ago

I had the idea of specifying the types directly via the invoke function by overwriting the type of invoke in buildtime generated dts. I was about to begin hacking away with Rust before I decided to look here first.

I imagine that the equivalent to OP's TS with this idea would be;

type CustomRequest = {
  message: String;
  request: Number;
}

type CustomResponse = {
  message: String;
  other_val: Number;
}

type TauriCommands = {
  my_first_custom_command: [
    return: string,
    args: { message: string },
  ],
  my_second_custom_command: [
    return: CustomResponse,
    args: {
      req: CustomRequest,
      hello: string,
    }
}

declare function invoke<T extends keyof TauriCommands>(cmd: T, args: T[1]): Promise<T[0]>;

This adds 0 JavaScript overhead as there isn't a wrapping function call; you call the same invoke as always, it's just more clear to the developer about what it's expecting.

I did make some breaking changes to the invoke type here;

To be non-breaking, the args thing is just a ?, for the generic;

declare function invoke<T extends U[0], U extends keyof TauriCommands>(cmd: U, args: U[1]): Promise<T>;

If we're voting on how generated types for commands are implemented in Tauri (I recognise that's not what we're doing), I vote types only. We're using Rust for these things specifically because JavaScript is slower and less well equipped for the job.

I would prefer to check back to my Rust files occasionally than to introduce boilerplate code and runtime overhead for the sake of a small bit of DevEx.

I wouldn't be against declare function typings for this if Tauri could come in and find usage, then replace them in compiled output with the actual invoke calls, but no real additional JS should be added; JS is too expensive to waste like this.

TeamDman commented 1 year ago

I've gotten pretty far in doing this in my own project.

For now I have a commands.rs file with all my Tauri command definitions. Inside a test I read this file and parse it with syn to build a type definition string. I have it ignoring State and AppHandle params in a pretty primitive way, in addition to expanding HashMap and Vec types into ts-friendly ones.

image

src-tauri/srd/commands.rs


#[cfg(test)]
mod test {

    fn rust_type_to_ts(rust_type: &syn::Type) -> String {
        match rust_type {
            syn::Type::Path(type_path) if type_path.qself.is_none() => {
                let ident = &type_path.path.segments.last().unwrap().ident;
                match ident.to_string().as_str() {
                    "str" => "string".to_owned(),
                    "()" => "void".to_owned(),
                    "Result" => {
                        match &type_path.path.segments.last().unwrap().arguments {
                            syn::PathArguments::AngleBracketed(angle_bracketed_data) => {
                                let args: Vec<_> = angle_bracketed_data.args.iter().collect();
                                if let syn::GenericArgument::Type(ty) = args[0] {
                                    rust_type_to_ts(ty)
                                } else {
                                    panic!("Result without inner type")
                                }
                            },
                            _ => panic!("Unsupported angle type: {}", ident.to_string()),
                        }
                    },
                    "Vec" => {
                        match &type_path.path.segments.last().unwrap().arguments {
                            syn::PathArguments::AngleBracketed(angle_bracketed_data) => {
                                if let Some(syn::GenericArgument::Type(ty)) = angle_bracketed_data.args.first() {
                                    format!("Array<{}>", rust_type_to_ts(ty))
                                } else {
                                    panic!("Vec without inner type")
                                }
                            },
                            _ => panic!("Unsupported angle type: {}", ident.to_string()),
                        }
                    },
                    "HashMap" => {
                        match &type_path.path.segments.last().unwrap().arguments {
                            syn::PathArguments::AngleBracketed(angle_bracketed_data) => {
                                let args: Vec<_> = angle_bracketed_data.args.iter().collect();
                                if let syn::GenericArgument::Type(key_ty) = args[0] {
                                    if let syn::GenericArgument::Type(value_ty) = args[1] {
                                        format!("Record<{}, {}>", rust_type_to_ts(key_ty), rust_type_to_ts(value_ty))
                                    } else {
                                        panic!("HashMap without value type")
                                    }
                                } else {
                                    panic!("HashMap without key type")
                                }
                            },
                            _ => panic!("Unsupported angle type: {}", ident.to_string()),
                        }
                    },
                    _ => ident.to_string(),
                }
            },
            syn::Type::Reference(type_reference) => {
                if let syn::Type::Path(type_path) = *type_reference.elem.clone() {
                    let ident = &type_path.path.segments.last().unwrap().ident;
                    match ident.to_string().as_str() {
                        "str" => "string".to_owned(),
                        _ => panic!("Unsupported type: &{}", ident.to_string()),
                    }
                } else {
                    panic!("Unsupported ref type: {}", quote::quote! {#type_reference}.to_string())
                }
            },
            syn::Type::Tuple(tuple_type) if tuple_type.elems.is_empty() => {
                "void".to_owned()
            },
            _ => panic!("Unsupported type: {}", quote::quote! {#rust_type}.to_string()),
        }
    }

    #[test]
    fn list_commands() {
        let contents = std::fs::read_to_string("src/commands.rs").unwrap();
        let ast = syn::parse_file(&contents).unwrap();

        let mut commands = Vec::new();

        for item in ast.items {
            if let syn::Item::Fn(item_fn) = item {
                let tauri_command_attr = item_fn.attrs.iter()
                    .find(|attr| {
                        attr.path().segments.iter().map(|seg| seg.ident.to_string()).collect::<Vec<_>>() == ["tauri", "command"]
                    });

                if tauri_command_attr.is_some() {
                    let command_name = item_fn.sig.ident.to_string();

                    let mut arg_types = Vec::new();
                    for arg in &item_fn.sig.inputs {
                        if let syn::FnArg::Typed(pat_type) = arg {
                            if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
                                // Filter out State and AppHandle parameters
                                let ty_string = quote::quote! {#pat_type.ty}.to_string();
                                if !ty_string.contains("State") && !ty_string.contains("AppHandle") {
                                    let ts_type = rust_type_to_ts(&pat_type.ty);
                                    arg_types.push(format!("{}: {}", pat_ident.ident, ts_type));
                                }
                            }
                        }
                    }

                    let return_type = if let syn::ReturnType::Type(_, ty) = &item_fn.sig.output {
                        rust_type_to_ts(ty)
                    } else {
                        String::new()
                    };

                    let command_definition = format!("    {}: [\n        return: {},\n        args: {{ {} }}\n    ]", command_name, return_type, arg_types.join(", "));
                    commands.push(command_definition);
                }
            }
        }

        let output = format!("type TauriCommands = {{\n{}\n}};", commands.join(",\n"));
        println!("{}", output);
    }

}

I'm still using ts_rs to generate types for stuff like my ConversationMessagePayload struct

src-tauri/src/payloads.rs

#[derive(Debug, TS, Serialize, Deserialize, Clone)]
#[ts(export, export_to = "../src/lib/bindings/")]
pub struct ConversationMessagePayload {
    #[ts(type="\"system\" | \"user\" | \"assistant\"")]
    pub author: chatgpt::types::Role,
    pub content: String,
}

src/lib/bindings/ConversationMessagePayload.ts

// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export interface ConversationMessagePayload { author: "system" | "user" | "assistant", content: string, }

but now I'm noticing that I haven't made a ConversationPayload struct for exporting the conversation type to the frontend, and am instead just returning the Conversation struct which is serializable but doesn't have the ts_rs exporting going on.

src-tauri/src/models.rs

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Conversation {
    pub id: uuid::Uuid,
    pub history: Vec<ConversationEventRecord>,
}

in this case it's fine since it's only for debugging

src/lib/Conversation.svelte

        invoke("get_conversation", {
            conversation_id: conversationId,
        }).then((data: any) => {
            console.log("got conversation debug info", data);
        });

it just means that there's an expectation that every type used in the TauriCommands type I'm generating is either a ts built-in or is exported by ts_rs

So right now this output is using a type that doesn't exist on my frontend yet

new_conversation: [
        return: Conversation,
        args: {  }
    ],

Right now my plan is to keep separate Payload types for everything that needs to be passed to the frontend, since the types used in my normal app logic may have properties that don't play nice with Serialize. Idk if I'm being confusing by not using MVC terminology, but it makes sense to me ¯\ (ツ)


tl;dr

TeamDman commented 1 year ago

Got it working, but then I realized that tauri-specta and tauri-bindgen exist, guess I should read better next time 🤦‍♂️

Here's my updated thing for reference, it still doesn't add imports for the user types tho xD

[dev-dependencies]
quote = "1.0.29"
syn = { version = "2.0.23", features = ["full"] }
indoc = "1.0.3"

#[cfg(test)]
mod test {

    fn rust_type_to_ts(rust_type: &syn::Type) -> String {
        match rust_type {
            syn::Type::Path(type_path) if type_path.qself.is_none() => {
                let ident = &type_path.path.segments.last().unwrap().ident;
                match ident.to_string().as_str() {
                    "str" => "string".to_owned(),
                    "String" => "string".to_owned(),
                    "()" => "void".to_owned(),
                    "Result" => match &type_path.path.segments.last().unwrap().arguments {
                        syn::PathArguments::AngleBracketed(angle_bracketed_data) => {
                            let args: Vec<_> = angle_bracketed_data.args.iter().collect();
                            if let syn::GenericArgument::Type(ty) = args[0] {
                                rust_type_to_ts(ty)
                            } else {
                                panic!("Result without inner type")
                            }
                        }
                        _ => panic!("Unsupported angle type: {}", ident.to_string()),
                    },
                    "Vec" => match &type_path.path.segments.last().unwrap().arguments {
                        syn::PathArguments::AngleBracketed(angle_bracketed_data) => {
                            if let Some(syn::GenericArgument::Type(ty)) =
                                angle_bracketed_data.args.first()
                            {
                                format!("Array<{}>", rust_type_to_ts(ty))
                            } else {
                                panic!("Vec without inner type")
                            }
                        }
                        _ => panic!("Unsupported angle type: {}", ident.to_string()),
                    },
                    "HashMap" => match &type_path.path.segments.last().unwrap().arguments {
                        syn::PathArguments::AngleBracketed(angle_bracketed_data) => {
                            let args: Vec<_> = angle_bracketed_data.args.iter().collect();
                            if let syn::GenericArgument::Type(key_ty) = args[0] {
                                if let syn::GenericArgument::Type(value_ty) = args[1] {
                                    format!(
                                        "Record<{}, {}>",
                                        rust_type_to_ts(key_ty),
                                        rust_type_to_ts(value_ty)
                                    )
                                } else {
                                    panic!("HashMap without value type")
                                }
                            } else {
                                panic!("HashMap without key type")
                            }
                        }
                        _ => panic!("Unsupported angle type: {}", ident.to_string()),
                    },
                    _ => ident.to_string(),
                }
            }
            syn::Type::Reference(type_reference) => {
                if let syn::Type::Path(type_path) = *type_reference.elem.clone() {
                    let ident = &type_path.path.segments.last().unwrap().ident;
                    match ident.to_string().as_str() {
                        "str" => "string".to_owned(),
                        _ => panic!("Unsupported type: &{}", ident.to_string()),
                    }
                } else {
                    panic!(
                        "Unsupported ref type: {}",
                        quote::quote! {#type_reference}.to_string()
                    )
                }
            }
            syn::Type::Tuple(tuple_type) if tuple_type.elems.is_empty() => "void".to_owned(),
            _ => panic!(
                "Unsupported type: {}",
                quote::quote! {#rust_type}.to_string()
            ),
        }
    }

    #[test]
    fn build_command_type_definitions() {
        let contents = std::fs::read_to_string("src/commands.rs").unwrap();
        let ast = syn::parse_file(&contents).unwrap();

        let mut commands = Vec::new();

        for item in ast.items {
            if let syn::Item::Fn(item_fn) = item {
                let tauri_command_attr = item_fn.attrs.iter().find(|attr| {
                    attr.path()
                        .segments
                        .iter()
                        .map(|seg| seg.ident.to_string())
                        .collect::<Vec<_>>()
                        == ["tauri", "command"]
                });

                if tauri_command_attr.is_some() {
                    let command_name = item_fn.sig.ident.to_string();

                    let mut arg_types = Vec::new();
                    for arg in &item_fn.sig.inputs {
                        if let syn::FnArg::Typed(pat_type) = arg {
                            if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
                                // Filter out State and AppHandle parameters
                                let ty_string = quote::quote! {#pat_type.ty}.to_string();
                                if !ty_string.contains("State") && !ty_string.contains("AppHandle")
                                {
                                    let ts_type = rust_type_to_ts(&pat_type.ty);
                                    arg_types.push(format!("{}: {}", pat_ident.ident, ts_type));
                                }
                            }
                        }
                    }

                    let return_type = if let syn::ReturnType::Type(_, ty) = &item_fn.sig.output {
                        rust_type_to_ts(ty)
                    } else {
                        String::new()
                    };

                    let command_definition = format!(
                        "    {}: {{\n        returns: {},\n        args: {{ {} }}\n    }}",
                        command_name,
                        return_type,
                        arg_types.join(", ")
                    );
                    commands.push(command_definition);
                }
            }
        }

        // build file contents
        let warning_header = "// THIS FILE IS AUTO-GENERATED BY CARGO TESTS! DO NOT EDIT!";
        let invoke_import = "import { invoke as invokeRaw } from \"@tauri-apps/api\";";
        let tauri_commands = format!("type TauriCommands = {{\n{}\n}};", commands.join(",\n"));
        let invoke_fn = indoc::indoc! {"
            export function invoke<T extends keyof TauriCommands>(cmd: T, args: TauriCommands[T][\"args\"]): Promise<TauriCommands[T][\"returns\"]> {
                return invokeRaw(cmd, args);
            }
        "};
        let output = format!(
            "{}\n\n{}\n\n{}\n\n{}",
            warning_header, invoke_import, tauri_commands, invoke_fn
        );

        // dump to file
        std::fs::create_dir_all("../src/lib/bindings").unwrap();
        let definitions_file =
            std::fs::File::create("../src/lib/bindings/tauri_commands.d.ts").unwrap();
        std::io::Write::write_all(
            &mut std::io::BufWriter::new(definitions_file),
            output.as_bytes(),
        )
        .unwrap();
    }
}

example output

// THIS FILE IS AUTO-GENERATED BY CARGO TESTS! DO NOT EDIT!

import { invoke as invokeRaw } from "@tauri-apps/api";

type TauriCommands = {
    list_conversation_titles: {
        returns: Record<string, string>,
        args: {  }
    },
    get_conversation: {
        returns: Conversation,
        args: { conversation_id: string }
    },
    get_conversation_title: {
        returns: string,
        args: { conversation_id: string }
    },
    get_conversation_messages: {
        returns: Array<ConversationMessagePayload>,
        args: { conversation_id: string }
    },
    new_conversation: {
        returns: Conversation,
        args: {  }
    },
    set_conversation_title: {
        returns: void,
        args: { conversation_id: string, new_title: string }
    },
    new_conversation_user_message: {
        returns: void,
        args: { conversation_id: string, content: string }
    },
    new_conversation_assistant_message: {
        returns: void,
        args: { conversation_id: string }
    },
    list_files: {
        returns: Array<string>,
        args: {  }
    }
};

export function invoke<T extends keyof TauriCommands>(cmd: T, args: TauriCommands[T]["args"]): Promise<TauriCommands[T]["returns"]> {
    return invokeRaw(cmd, args);
}
Brendonovich commented 1 year ago

@TeamDman Was just about to recommend looking at tauri-specta haha. It does the 'macro/decorator or something' you mentioned and works with any type that you derive(specta::Type) for, not just hardcoded ones.

On that note, I feel like this pretty much solved. While there's no officially recommended solution yet, there's plenty out there to generate TypeScript bindings to Tauri commands.

Rust -> TypeScript

tauri-specta is your crate. It utilises specta to extract type information via the Type trait, and then generates individual functions that mirror your Tauri commands, along with all custom types that those commands take as args and return. It's the easiest answer to 'how do I generate TypeScript bindings from Rust'.

Schema -> Rust + TypeScript

tauri-bindgen allows you to define your commands + custom types in *.wit files, and then generate both Rust for you to implement your commands, and many guest languages as bindings for your frontend. It allows for a more centralised approach to defining your commands than just using whatever is defined in Rust.

Other Solutions

If there's any other good solutions I'm missing then please let me know!

michTheBrandofficial commented 2 months ago

Goal: Atomatically generate Typescript functions for Rust commands in Typescript environment

Why: Use Typescript with type checking for Rust commands rather than invoke('some string', somedata)

How:

Assume these Rust commands are available

#[tauri::command]
async fn my_first_custom_command(message: String) -> Result<String, String> {
  Ok("simple type command".into())
}

#[derive(serde::Serialize)]
struct CustomResponse {
  message: String,
  other_val: usize,
}

#[derive(serde::Deserialize)]
struct CustomRequest {
  message: String,
  request: usize,
}

#[tauri::command]
async fn my_second_custom_command(req: CustomRequest, hello: String) -> Result<CustomResponse, String> {
  Ok(CustomResponse {
    message: "custom response".into(),
    other_val: 42,
  })
}

Using Typescript, we can create functions that conform to the Rust commands:

import tauriapi from "@tauri-apps/api";

const { invoke } = tauriapi.tauri;

function my_first_custom_command(message: String): Promise<String> {
  return invoke('my_first_custom_command', {
    message
  })
}

type CustomRequest = {
  message: String;
  request: Number;
}

type CustomResponse = {
  message: String;
  other_val: Number;
}

function my_second_custom_command(req: CustomRequest, hello: String): Promise<CustomResponse>{
  return invoke('my_second_custom_command', {req, hello})
}

export {
  my_first_custom_command,
  my_second_custom_command
}

Now we can call my_first_custom_command and my_second_custom_command with type checking in Typescript.

my_first_custom_command("this must be string")
.then((res) => {
  // res is String
  console.log(res);
});

my_second_custom_command({
  message: "this must be CustomRequest",
  request: 123,
}, "this must be string")
.then((res) => {
  // res is CustomResponse
  console.log(res.message, res.other_val);
});

The steps can be:

  • Read the tauri::commands function in Rust
  • Note the input parameters and output type
  • Generate the corresponding type and function in typescript

Problems that may arises:

  • Rust commands are written into different files
  • Rust commands use Struct defined in other files
  • The Typescript generated file(s) must be re-generated on each rebuild

Simple solution to the above solutions:

  • Rust commands are placed into a Rust module (src-tauri/src/tauri_commands/*.rs)
  • Typescript generated files are placed into a seperated modules (tauri_commands/*.tsx)
  • yarn tauri dev generates Typescript files everytime a file in src-tauri/src/tauri_commands is modified

In discord chat, Denjell suggest that a WASM function can be generated from these Rust commands.

Originated from: https://discord.com/channels/616186924390023171/731495047962558564/832483720728412192

I just started using Tauri today and would love to see this in action. Has anyone done this?

FabianLars commented 2 months ago

@michTheBrandofficial Please see Brendonovich's comment right above yours... tauri-specta is currently the blessed solution.