From 410c949b87424b4ac0df5e3f38930781c6eda147 Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Fri, 23 Jan 2026 20:56:19 +0800 Subject: feat(client): add tauri api macros --- crates/macros/Cargo.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 crates/macros/Cargo.toml (limited to 'crates/macros/Cargo.toml') diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml new file mode 100644 index 0000000..cd5b4a6 --- /dev/null +++ b/crates/macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "dropout-macros" +version = "0.1.0" +edition = "2021" +description = "Proc-macro crate providing #[dropout::api] for generating Tauri TypeScript bindings" +license = "MIT OR Apache-2.0" +publish = false + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full"] } +heck = "0.4" -- cgit v1.2.3-70-g09d2 From e0dd58c443ed4d13e69b511bb01e17922926076f Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Sun, 15 Feb 2026 15:29:49 +0800 Subject: refactor(macros): use darling to parse macro metas --- crates/macros/Cargo.toml | 1 + crates/macros/src/attr.rs | 10 ++++++++++ crates/macros/src/lib.rs | 41 +++++++++++++++-------------------------- 3 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 crates/macros/src/attr.rs (limited to 'crates/macros/Cargo.toml') diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index cd5b4a6..e5b9ad7 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -14,3 +14,4 @@ proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["full"] } heck = "0.4" +darling = { version = "0.23.0", features = ["serde"] } diff --git a/crates/macros/src/attr.rs b/crates/macros/src/attr.rs new file mode 100644 index 0000000..ef1df86 --- /dev/null +++ b/crates/macros/src/attr.rs @@ -0,0 +1,10 @@ +use darling::FromMeta; + +#[derive(Default, FromMeta)] +#[darling(default)] +#[darling(derive_syn_parse)] +pub struct MacroArgs { + pub export_to: Option, + pub export_to_path: Option, + pub import_from: Option, +} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 1b26bf2..9e310ca 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -6,6 +6,10 @@ use syn::{ Expr, FnArg, Ident, ItemFn, Lit, MetaNameValue, Pat, PathArguments, ReturnType, Type, }; +use crate::attr::MacroArgs; + +mod attr; + fn get_lit_str_value(nv: &MetaNameValue) -> Option { // In syn v2 MetaNameValue.value is an Expr (usually Expr::Lit). Extract string literal if present. match &nv.value { @@ -180,35 +184,20 @@ fn snake_to_camel(s: &str) -> String { #[proc_macro_attribute] pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { - // Parse inputs as a punctuated list of MetaNameValue (e.g. export_to = "...", import_from = "...") - // `MetaList` implements `Parse` so we can parse the raw attribute token stream reliably - struct MetaList(Punctuated); - impl Parse for MetaList { - fn parse(input: ParseStream) -> syn::Result { - Ok(MetaList(Punctuated::parse_terminated(input)?)) - } - } - let metas = parse_macro_input!(attr as MetaList).0; + // Parse attribute args via `darling` crate + let meta: MacroArgs = match syn::parse(attr) { + Ok(meta) => meta, + Err(err) => return err.into_compile_error().into(), + }; let input_fn = parse_macro_input!(item as ItemFn); // Extract attribute args: export_to, import_from - let mut export_to: Option = None; - let mut import_from: Option = None; - - for nv in metas.iter() { - if let Some(ident) = nv.path.get_ident() { - let name = ident.to_string(); - if name == "export_to" { - if let Some(v) = get_lit_str_value(nv) { - export_to = Some(v); - } - } else if name == "import_from" { - if let Some(v) = get_lit_str_value(nv) { - import_from = Some(v); - } - } - } - } + let export_to = match (meta.export_to, meta.export_to_path) { + (Some(to), None) => Some(to), + (_, Some(path)) => Some(path), + (None, None) => None, + }; + let import_from = meta.import_from; // Analyze function let fn_name_ident: Ident = input_fn.sig.ident.clone(); -- cgit v1.2.3-70-g09d2 From 2c3de3ac5ab1ab59f7245ab9cbdfda9b4e96dcb0 Mon Sep 17 00:00:00 2001 From: 苏向夜 Date: Sun, 15 Feb 2026 15:34:40 +0800 Subject: merge: dev --- crates/macros/Cargo.toml | 5 +- crates/macros/src/lib.rs | 159 +++++++++++++++++++++++------------------------ src-tauri/Cargo.toml | 60 ++++++++++-------- src-tauri/src/main.rs | 1 + 4 files changed, 115 insertions(+), 110 deletions(-) (limited to 'crates/macros/Cargo.toml') diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index e5b9ad7..729b557 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -10,8 +10,9 @@ publish = false proc-macro = true [dependencies] +darling = { version = "0.23.0", features = ["serde"] } +heck = "0.4" +inventory = "0.3" proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["full"] } -heck = "0.4" -darling = { version = "0.23.0", features = ["serde"] } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 9e310ca..16db1a9 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,3 +1,4 @@ +use heck::ToLowerCamelCase; use proc_macro::TokenStream; use quote::quote; use std::collections::BTreeSet; @@ -24,7 +25,7 @@ fn get_lit_str_value(nv: &MetaNameValue) -> Option { } } -fn is_state_or_window(ty: &Type) -> bool { +fn is_tauri_native(ty: &Type) -> bool { // Unwrap reference let mut t = ty; if let Type::Reference(r) = t { @@ -112,7 +113,7 @@ fn rust_type_to_ts(ty: &Type) -> (String, bool) { let (inner_ts, inner_struct) = rust_type_to_ts(inner); return (format!("{}[]", inner_ts), inner_struct); } - ("any[]".to_string(), false) + ("unknown[]".to_string(), false) } other => { // treat as struct/complex type @@ -121,7 +122,7 @@ fn rust_type_to_ts(ty: &Type) -> (String, bool) { }; } } - ("any".to_string(), false) + ("unknown".to_string(), false) } fn get_return_ts(ty: &ReturnType) -> (String, BTreeSet) { @@ -158,30 +159,11 @@ fn get_return_ts(ty: &ReturnType) -> (String, BTreeSet) { } } // fallback - ("Promise".to_string(), imports) + ("Promise".to_string(), imports) } } } -fn snake_to_camel(s: &str) -> String { - let mut parts = s.split('_'); - let mut out = String::new(); - if let Some(first) = parts.next() { - out.push_str(&first.to_ascii_lowercase()); - } - for p in parts { - if p.is_empty() { - continue; - } - let mut chs = p.chars(); - if let Some(c) = chs.next() { - out.push_str(&c.to_ascii_uppercase().to_string()); - out.push_str(&chs.as_str().to_ascii_lowercase()); - } - } - out -} - #[proc_macro_attribute] pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { // Parse attribute args via `darling` crate @@ -202,7 +184,7 @@ pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { // Analyze function let fn_name_ident: Ident = input_fn.sig.ident.clone(); let fn_name = fn_name_ident.to_string(); - let ts_fn_name = snake_to_camel(&fn_name); + let ts_fn_name = fn_name.to_lower_camel_case(); // Collect parameters (ignore State/Window) let mut param_names: Vec = Vec::new(); @@ -225,7 +207,7 @@ pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { }; // Check if type should be ignored (State, Window) - if is_state_or_window(&*pt.ty) { + if is_tauri_native(&*pt.ty) { continue; } @@ -240,7 +222,7 @@ pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { if let Some(pn) = param_ident { // Convert param name to camelCase - keep existing but ensure camelCase for TS // We'll convert snake_case param names to camelCase - let ts_param_name = snake_to_camel(&pn); + let ts_param_name = pn.to_lower_camel_case(); param_names.push(ts_param_name.clone()); param_defs.push(format!("{}: {}", ts_param_name, ts_type)); } @@ -255,21 +237,21 @@ pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { } // Build TypeScript code string - let mut ts_lines: Vec = Vec::new(); - - ts_lines.push(r#"import { invoke } from "@tauri-apps/api/core""#.to_string()); - - if !import_types.is_empty() { - if let Some(import_from_str) = import_from.clone() { - let types_joined = import_types.iter().cloned().collect::>().join(", "); - ts_lines.push(format!( - "import {{ {} }} from \"{}\"", - types_joined, import_from_str - )); - } else { - // If no import_from provided, still import types from local path? We'll skip if not provided. - } - } + // let mut ts_lines: Vec = Vec::new(); + + // ts_lines.push(r#"import { invoke } from "@tauri-apps/api/core""#.to_string()); + + // if !import_types.is_empty() { + // if let Some(import_from_str) = import_from.clone() { + // let types_joined = import_types.iter().cloned().collect::>().join(", "); + // ts_lines.push(format!( + // "import {{ {} }} from \"{}\"", + // types_joined, import_from_str + // )); + // } else { + // // If no import_from provided, still import types from local path? We'll skip if not provided. + // } + // } // function signature let params_sig = param_defs.join(", "); @@ -285,7 +267,7 @@ pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { if return_ts_promise.starts_with("Promise<") && return_ts_promise.ends_with('>') { &return_ts_promise["Promise<".len()..return_ts_promise.len() - 1] } else { - "any" + "unknown" }; let invoke_line = if param_names.is_empty() { @@ -297,14 +279,14 @@ pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { ) }; - ts_lines.push(format!( - "export async function {}({}): {} {{", - ts_fn_name, params_sig, return_ts_promise - )); - ts_lines.push(invoke_line); - ts_lines.push("}".to_string()); + // ts_lines.push(format!( + // "export async function {}({}): {} {{", + // ts_fn_name, params_sig, return_ts_promise + // )); + // ts_lines.push(invoke_line); + // ts_lines.push("}".to_string()); - let ts_contents = ts_lines.join("\n") + "\n"; + // let ts_contents = ts_lines.join("\n") + "\n"; // Prepare test function name let test_fn_name = Ident::new( @@ -320,38 +302,52 @@ pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { // Build tokens let original_fn = &input_fn; - let ts_string_literal = ts_contents.clone(); - - let write_stmt = if export_to_literal.is_empty() { - // No-op: don't write - // quote! { - // // No export_to provided; skipping file write. - // } - panic!("No export_to provided") - } else { - // We'll append to the file to avoid overwriting existing bindings from other macros. - // Use create(true).append(true) - let path = export_to_literal.clone(); - let ts_lit = syn::LitStr::new(&ts_string_literal, proc_macro2::Span::call_site()); - quote! { - { - // Ensure parent directories exist if possible (best-effort) - let path = std::path::Path::new(#path); - if let Some(parent) = path.parent() { - let _ = std::fs::create_dir_all(parent); - } - // Append generated bindings to file - match OpenOptions::new().create(true).append(true).open(path) { - Ok(mut f) => { - let _ = f.write_all(#ts_lit.as_bytes()); - println!("Successfully wrote to {}", path.display()); - } - Err(e) => { - eprintln!("dropout::api binding write failed: {}", e); - } - } - } + // let ts_string_literal = ts_contents.clone(); + + // let write_stmt = if export_to_literal.is_empty() { + // // No-op: don't write + // // quote! { + // // // No export_to provided; skipping file write. + // // } + // panic!("No export_to provided") + // } else { + // // We'll append to the file to avoid overwriting existing bindings from other macros. + // // Use create(true).append(true) + // let path = export_to_literal.clone(); + // let ts_lit = syn::LitStr::new(&ts_string_literal, proc_macro2::Span::call_site()); + // quote! { + // { + // // Ensure parent directories exist if possible (best-effort) + // let path = std::path::Path::new(#path); + // if let Some(parent) = path.parent() { + // let _ = std::fs::create_dir_all(parent); + // } + // // Append generated bindings to file + // match OpenOptions::new().create(true).append(true).open(path) { + // Ok(mut f) => { + // let _ = f.write_all(#ts_lit.as_bytes()); + // println!("Successfully wrote to {}", path.display()); + // } + // Err(e) => { + // eprintln!("dropout::api binding write failed: {}", e); + // } + // } + // } + // } + // }; + let register_stmt = quote! { + ::dropout_core::inventory::submit! { + ::dropout_core::ApiInfo { + fn_name: #fn_name, + ts_fn_name: #ts_fn_name, + param_names: vec![#(#param_names),*], + param_defs: vec![#(#param_defs),*], + return_ts_promise: #return_ts_promise, + import_types: BTreeSet::from([#(#import_types),*]), + import_from: #import_from, + export_to: #export_to_literal, } + } }; let gen = quote! { @@ -366,7 +362,8 @@ pub fn api(attr: TokenStream, item: TokenStream) -> TokenStream { #[test] fn #test_fn_name() { // Generated TypeScript bindings for function: #fn_name - #write_stmt + // #write_stmt + #register_stmt } } }; diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cf92627..28d600a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,47 +1,53 @@ [package] name = "dropout" version = "0.2.0-alpha.1" -edition = "2021" +edition = "2024" authors = ["HsiangNianian"] description = "The DropOut Minecraft Game Launcher" -license = "MIT" repository = "https://github.com/HydroRoll-Team/DropOut" +license = "MIT" publish = false +[package.metadata.deb] +depends = "libgtk-3-0" +section = "games" +assets = [ + ["target/release/dropout", "usr/bin/", "755"], +] + [dependencies] -dropout-macros = { version = "0.1.0", path = "../crates/macros" } -serde = { version = "1.0", features = ["derive"] } -toml = "0.5" -log = "0.4" +bytes = "1.11.0" +chrono = "0.4" +dirs = "5.0" +dropout-core = { path = "../crates/core", version = "0.1.0" } +dropout-macros = { path = "../crates/macros", version = "0.1.0" } env_logger = "0.9" -tokio = { version = "1.49.0", features = ["full"] } -reqwest = { version = "0.11", features = ["json", "blocking", "stream", "multipart"] } -serde_json = "1.0.149" -tauri = { version = "2.9", features = [] } -tauri-plugin-shell = "2.3" -uuid = { version = "1.10.0", features = ["v3", "v4", "serde"] } +flate2 = "1.0" futures = "0.3" +hex = "0.4" +log = "0.4" +regex = "1.12.2" +reqwest = { version = "0.11", features = [ + "blocking", + "json", + "multipart", + "stream" +] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.149" +serde_urlencoded = "0.7.1" sha1 = "0.10" sha2 = "0.10" -hex = "0.4" -zip = "2.2.2" -flate2 = "1.0" tar = "0.4" -dirs = "5.0" -serde_urlencoded = "0.7.1" +tauri = { version = "2.9", features = [] } tauri-plugin-dialog = "2.6.0" tauri-plugin-fs = "2.4.5" -bytes = "1.11.0" -chrono = "0.4" -regex = "1.12.2" +tauri-plugin-shell = "2.3" +tokio = { version = "1.49.0", features = ["full"] } +toml = "0.5" ts-rs = { version = "11.1.0", features = ["serde-compat"] } +uuid = { version = "1.10.0", features = ["serde", "v3", "v4"] } +zip = "2.2.2" [build-dependencies] tauri-build = { version = "2.0", features = [] } - -[package.metadata.deb] -depends = "libgtk-3-0" -section = "games" -assets = [ - ["target/release/dropout", "usr/bin/", "755"], -] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b74c746..f652012 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -896,6 +896,7 @@ fn parse_jvm_arguments( } #[tauri::command] +#[dropout_macros::api] async fn get_versions( _window: Window, instance_state: State<'_, core::instance::InstanceState>, -- cgit v1.2.3-70-g09d2 From 2ed79c171c849338343edddafe68bc1759adf8b8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Feb 2026 18:24:11 +0000 Subject: chore(release): bump versions --- .changes/dropout-macros.md | 5 ----- .changes/partial-react.md | 5 ----- .changes/ts-bindings.md | 5 ----- crates/macros/CHANGELOG.md | 7 +++++++ crates/macros/Cargo.toml | 2 +- packages/ui/CHANGELOG.md | 7 +++++++ packages/ui/package.json | 4 ++-- src-tauri/CHANGELOG.md | 6 ++++++ src-tauri/Cargo.toml | 4 ++-- src-tauri/tauri.conf.json | 2 +- 10 files changed, 26 insertions(+), 21 deletions(-) delete mode 100644 .changes/dropout-macros.md delete mode 100644 .changes/partial-react.md delete mode 100644 .changes/ts-bindings.md create mode 100644 crates/macros/CHANGELOG.md create mode 100644 packages/ui/CHANGELOG.md (limited to 'crates/macros/Cargo.toml') diff --git a/.changes/dropout-macros.md b/.changes/dropout-macros.md deleted file mode 100644 index faa4e3e..0000000 --- a/.changes/dropout-macros.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -dropout-macros: "patch:feat" ---- - -Add `dropout-macros` crate to generate tauri api wrappers automatically. diff --git a/.changes/partial-react.md b/.changes/partial-react.md deleted file mode 100644 index 62ca46d..0000000 --- a/.changes/partial-react.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@dropout/ui": "minor:refactor" ---- - -Partial rewrite UI to react port. diff --git a/.changes/ts-bindings.md b/.changes/ts-bindings.md deleted file mode 100644 index 01be1b1..0000000 --- a/.changes/ts-bindings.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -dropout: "patch:feat" ---- - -Add `ts-rs` for generating TypeScript bindings. diff --git a/crates/macros/CHANGELOG.md b/crates/macros/CHANGELOG.md new file mode 100644 index 0000000..2e8da6a --- /dev/null +++ b/crates/macros/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## v0.1.0-alpha.0 + +### New Features + +- [`410c949`](https://github.com/HydroRoll-Team/DropOut/commit/410c949b87424b4ac0df5e3f38930781c6eda147): Add `dropout-macros` crate to generate tauri api wrappers automatically. ([#77](https://github.com/HydroRoll-Team/DropOut/pull/77) by @HsiangNianian) diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index 729b557..54f64f7 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dropout-macros" -version = "0.1.0" +version = "0.1.0-alpha.0" edition = "2021" description = "Proc-macro crate providing #[dropout::api] for generating Tauri TypeScript bindings" license = "MIT OR Apache-2.0" diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md new file mode 100644 index 0000000..28abb60 --- /dev/null +++ b/packages/ui/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## v0.0.0-alpha.0 + +### Refactors + +- [`66668d8`](https://github.com/HydroRoll-Team/DropOut/commit/66668d85d603c5841d755a6023aa1925559fc6d4): Partial rewrite UI to react port. ([#77](https://github.com/HydroRoll-Team/DropOut/pull/77) by @HsiangNianian) diff --git a/packages/ui/package.json b/packages/ui/package.json index c78290f..e967bce 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,7 @@ { "name": "@dropout/ui", "private": true, - "version": "0.0.0", + "version": "0.0.0-alpha.0", "type": "module", "scripts": { "dev": "vite", @@ -52,4 +52,4 @@ "typescript": "~5.9.3", "vite": "npm:rolldown-vite@^7" } -} +} \ No newline at end of file diff --git a/src-tauri/CHANGELOG.md b/src-tauri/CHANGELOG.md index 4b2d22b..90861d9 100644 --- a/src-tauri/CHANGELOG.md +++ b/src-tauri/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v0.2.0-alpha.2 + +### New Features + +- [`ef56081`](https://github.com/HydroRoll-Team/DropOut/commit/ef560813c68c113325d8d84ff13cd419eb6583df): Add `ts-rs` for generating TypeScript bindings. ([#77](https://github.com/HydroRoll-Team/DropOut/pull/77) by @HsiangNianian) + ## v0.2.0-alpha.1 ### New Features diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 29da3a1..5ba731c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dropout" -version = "0.2.0-alpha.1" +version = "0.2.0-alpha.2" edition = "2024" authors = ["HsiangNianian"] description = "The DropOut Minecraft Game Launcher" @@ -19,7 +19,7 @@ assets = [ bytes = "1.11.0" chrono = "0.4" dirs = "5.0" -dropout-macros = { path = "../crates/macros", version = "0.1.0" } +dropout-macros = { path = "../crates/macros", version = "0.1.0-alpha.0" } env_logger = "0.9" flate2 = "1.0" futures = "0.3" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1f7ebe9..96ac48b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "dropout", - "version": "0.2.0-alpha.1", + "version": "0.2.0-alpha.2", "identifier": "com.dropout.launcher", "build": { "beforeDevCommand": "pnpm --filter @dropout/ui dev", -- cgit v1.2.3-70-g09d2