tauri-helper 0.2.1

A collection of tools and utilities designed to simplify the development of Tauri applications.
Documentation
use rayon::prelude::*;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use syn::parse_file;
use tauri_helper_core::{find_workspace_dir, get_workspace, get_workspace_members};
use walkdir::WalkDir;

pub use tauri_helper_core::types::TauriHelperOptions;
pub use tauri_helper_macros::*;

#[allow(clippy::needless_doctest_main)]
/// Scans the crate for functions annotated with `#[tauri::command]` and optionally `#[auto_collect_command]`,
/// then generates a file containing a list of these functions in the `tauri_commands_list` folder.
///
/// This function is intended to be used in a `build.rs` script to automate the process of
/// collecting Tauri commands during the build process. It should be called before invoking
/// `tauri_build::build()` to ensure the command list is available for the Tauri application.
///
/// # Usage
///
/// Add the following to your `build.rs` file:
///
/// ```rust
/// fn main() {
///     // Generate the command file for Tauri
///     tauri_helper::generate_command_file(tauri_helper::TauriHelperOptions::default());
///
///     // Build the Tauri application
///     tauri_build::build();
/// }
/// ```
///
/// # Annotations
///
/// By default, this function looks for functions annotated with both `#[tauri::command]` and
/// `#[auto_collect_command]`. For example:
///
/// ```rust
/// #[tauri::command]
/// #[auto_collect_command]
/// fn my_command() {
///     println!("Some Command")
/// }
/// ```
///
/// These functions will be automatically collected and written to the `tauri_commands_list` folder.
///
/// If the `collect_all` option is set to `true`, the function will collect all `#[tauri::command]`
/// functions, regardless of whether they have the `#[auto_collect_command]` attribute. However,
/// this behavior is not recommended unless explicitly needed.
///
/// # Output
///
/// The generated file will be placed in the `tauri_commands_list` folder (relative to the crate root) inside of the target folder.
/// The file will contain a list of all collected commands, which can be used by the Tauri application
/// to register commands.
///
/// # Options
///
/// The behavior of this function can be customized using the `TauriHelperOptions` struct:
///
/// - **`collect_all`**: When `true`, collects all `#[tauri::command]` functions, even if they lack
///   the `#[auto_collect_command]` attribute. When `false` (default), only functions with both
///   `#[tauri::command]` and `#[auto_collect_command]` are collected.
///
///   **Recommendation**: Keep this option set to `false` to ensure explicit control over which
///   commands are included in your Tauri application.
///
/// # Notes
///
/// - This function should only be called once per build, typically in the `build.rs` script.
/// - More options are coming such as a list of explicit files that need to be scanned only, if you have any more ideas, please open an issue on Github.
///
/// # Example
///
/// ```rust
/// #[tauri::command]
/// #[auto_collect_command]
/// fn greet(name: String) -> String {
///     format!("Hello, {}!", name)
/// }
///
/// #[tauri::command]
/// fn calculate_sum(a: i32, b: i32) -> i32 {
///     a + b
/// }
/// ```
///
/// With `collect_all` set to `false` (default), only `greet` will be collected. With `collect_all`
/// set to `true`, both `greet` and `calculate_sum` will be collected.
///
/// # Panics
///
/// This function will panic if:
/// - The `tauri_commands_list` folder cannot be created or written to.
/// - No functions matching the criteria are found.
///
/// # Errors
///
/// If the function encounters an error during file generation, it will log the error and exit the
/// build process with a non-zero status code.
pub fn generate_command_file(options: TauriHelperOptions) {
    let workspace_root = find_workspace_dir(Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()));
    let commands_dir = workspace_root.join("target").join("tauri_commands_list");
    fs::create_dir_all(&commands_dir).unwrap();

    let workspace_members = options
        .members
        .clone()
        .unwrap_or_else(|| get_workspace_members(&workspace_root));

    for member in &workspace_members {
        println!("cargo:rerun-if-changed={}", member);
    }

    workspace_members.par_iter().for_each(|member| {
        let manifest_dir = workspace_root.join(member);
        let crate_name = manifest_dir
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or_default()
            .to_string();

        let src_dir = manifest_dir.join("src");
        let mut functions = Vec::new();

        let rs_files: Vec<_> = WalkDir::new(&src_dir)
            .into_iter()
            .filter_map(|e| e.ok())
            .filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("rs"))
            .collect();

        let file_fns: Vec<String> = rs_files
            .par_iter()
            .filter_map(|entry| {
                let path = entry.path();
                let content = fs::read_to_string(path).ok()?;
                if !content.contains("tauri::command") && !content.contains("auto_collect_command")
                {
                    return None;
                }

                let ast = parse_file(&content).ok()?;
                let mut found = Vec::new();

                for item in ast.items {
                    if let syn::Item::Fn(func) = item {
                        if !options.collect_all {
                            if func
                                .attrs
                                .iter()
                                .any(|a| a.path().is_ident("auto_collect_command"))
                            {
                                found.push(func.sig.ident.to_string());
                            }
                        } else if func.attrs.iter().any(|a| {
                            let p = a.path();
                            p.is_ident("command")
                                || (p.segments.len() == 2
                                    && p.segments[0].ident == "tauri"
                                    && p.segments[1].ident == "command")
                        }) {
                            found.push(func.sig.ident.to_string());
                        }
                    }
                }
                Some(found)
            })
            .flatten()
            .collect();

        functions.extend(file_fns);

        if !functions.is_empty() {
            let package_name = get_workspace().package.name.replace('-', "_");
            let command_file = commands_dir.join(format!("{}.txt", crate_name));
            let mut file = File::create(&command_file).unwrap();

            for func in functions {
                let full_name = if crate_name.replace('-', "_") == "src_tauri" {
                    format!("{}::{}", package_name, func)
                } else {
                    format!("{}::{}", crate_name.replace('-', "_"), func)
                };
                writeln!(file, "{}", full_name).unwrap();
            }
        }
    });
}