From 24d2ce3cd9465d570d2be953d006eeceb1710786 Mon Sep 17 00:00:00 2001 From: Makito Date: Sun, 6 Jul 2025 00:22:31 +0900 Subject: [PATCH] refactor(stage-tamagotchi): type-safe commands codegen poc (#258) * refactor(stage-tamagotchi): type-safe commands codegen poc * chore: remove the useless comment * chore: [suggestion] remove comments Co-authored-by: Neko * chore: [suggestion] remove comments Co-authored-by: Neko * chore: [suggestion] remove comments Co-authored-by: Neko --------- Co-authored-by: Neko --- Cargo.lock | 82 ++++++++++++++++ apps/stage-tamagotchi/src-tauri/Cargo.toml | 3 + .../window_pass_through_on_hover/mod.rs | 67 +++++++++---- .../src/plugins/window_router_link/mod.rs | 38 ++++++-- .../bindings/window-pass-through-on-hover.ts | 96 +++++++++++++++++++ .../commands/bindings/window-router-link.ts | 88 +++++++++++++++++ apps/stage-tamagotchi/src/commands/index.ts | 2 + .../src/components/Tauri/WindowRouterLink.vue | 9 +- apps/stage-tamagotchi/src/tauri/invoke.ts | 7 -- apps/stage-tamagotchi/src/utils/windows.ts | 8 +- cspell.config.yaml | 1 + eslint.config.ts | 1 + 12 files changed, 358 insertions(+), 44 deletions(-) create mode 100644 apps/stage-tamagotchi/src/commands/bindings/window-pass-through-on-hover.ts create mode 100644 apps/stage-tamagotchi/src/commands/bindings/window-router-link.ts create mode 100644 apps/stage-tamagotchi/src/commands/index.ts diff --git a/Cargo.lock b/Cargo.lock index 814305af1..de55f91a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "addr2line" version = "0.24.2" @@ -198,6 +204,8 @@ dependencies = [ "rubato", "serde", "serde_json", + "specta", + "specta-typescript", "symphonia", "tauri", "tauri-build", @@ -208,6 +216,7 @@ dependencies = [ "tauri-plugin-positioner", "tauri-plugin-prevent-default", "tauri-plugin-window-state", + "tauri-specta", "tokenizers", "tokio", "url", @@ -5247,6 +5256,50 @@ dependencies = [ "system-deps", ] +[[package]] +name = "specta" +version = "2.0.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971" +dependencies = [ + "paste", + "specta-macros", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0074b9e30ed84c6924eb63ad8d2fe71cdc82628525d84b1fcb1f2fd40676517" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "specta-serde" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77216504061374659e7245eac53d30c7b3e5fe64b88da97c753e7184b0781e63" +dependencies = [ + "specta", + "thiserror 1.0.69", +] + +[[package]] +name = "specta-typescript" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3220a0c365e51e248ac98eab5a6a32f544ff6f961906f09d3ee10903a4f52b2d" +dependencies = [ + "specta", + "specta-serde", + "thiserror 1.0.69", +] + [[package]] name = "spm_precompiled" version = "0.1.4" @@ -5700,6 +5753,7 @@ dependencies = [ "serde_json", "serde_repr", "serialize-to-javascript", + "specta", "swift-rs", "tauri-build", "tauri-macros", @@ -5960,6 +6014,34 @@ dependencies = [ "wry", ] +[[package]] +name = "tauri-specta" +version = "2.0.0-rc.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23c0132dd3cf6064e5cd919b82b3f47780e9280e7b5910babfe139829b76655" +dependencies = [ + "heck 0.5.0", + "serde", + "serde_json", + "specta", + "specta-typescript", + "tauri", + "tauri-specta-macros", + "thiserror 2.0.12", +] + +[[package]] +name = "tauri-specta-macros" +version = "2.0.0-rc.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4aa93823e07859546aa796b8a5d608190cd8037a3a5dce3eb63d491c34bda8" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.103", +] + [[package]] name = "tauri-utils" version = "2.5.0" diff --git a/apps/stage-tamagotchi/src-tauri/Cargo.toml b/apps/stage-tamagotchi/src-tauri/Cargo.toml index dcf754656..1459d18fc 100644 --- a/apps/stage-tamagotchi/src-tauri/Cargo.toml +++ b/apps/stage-tamagotchi/src-tauri/Cargo.toml @@ -51,6 +51,9 @@ tokenizers = "0.21.2" url = "2.5.4" tauri-plugin-window-state = "2.3.0" tauri-plugin-positioner = "2.3.0" +specta = "=2.0.0-rc.22" +specta-typescript = "0.0.9" +tauri-specta = { version = "=2.0.0-rc.21", features = ["derive", "typescript"] } [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.1" diff --git a/apps/stage-tamagotchi/src-tauri/src/plugins/window_pass_through_on_hover/mod.rs b/apps/stage-tamagotchi/src-tauri/src/plugins/window_pass_through_on_hover/mod.rs index 4c5588d76..db88e3f6d 100644 --- a/apps/stage-tamagotchi/src-tauri/src/plugins/window_pass_through_on_hover/mod.rs +++ b/apps/stage-tamagotchi/src-tauri/src/plugins/window_pass_through_on_hover/mod.rs @@ -9,6 +9,8 @@ use std::{sync::atomic::Ordering, time::Duration}; use native_macos::{get_mouse_location, get_window_frame}; #[cfg(target_os = "windows")] use native_windows::{get_mouse_location, get_window_frame}; +#[cfg(debug_assertions)] +use specta_typescript::Typescript; use state::{WindowClickThroughState, set_pass_through_enabled}; use tauri::{ Emitter, @@ -89,34 +91,65 @@ async fn stop_monitor(window: tauri::Window) -> Result<()> { } #[tauri::command] -async fn start_pass_through(window: tauri::Window) -> Result<()> { - set_pass_through_enabled(&window, true).unwrap_or_else(|e| { +#[specta::specta] +async fn start_pass_through( + window: tauri::Window +) -> std::result::Result<(), String> { + set_pass_through_enabled(&window, true).map_err(|e| { log::error!("Failed to enable click-through: {e}"); - }); - - Ok(()) + e + }) } #[tauri::command] -async fn stop_pass_through(window: tauri::Window) -> Result<()> { - set_pass_through_enabled(&window, false).unwrap_or_else(|e| { +#[specta::specta] +async fn stop_pass_through( + window: tauri::Window +) -> std::result::Result<(), String> { + set_pass_through_enabled(&window, false).map_err(|e| { log::error!("Failed to disable click-through: {e}"); - }); - - Ok(()) + e + }) } +const PLUGIN_NAME: &str = "proj-airi-tauri-plugin-window-pass-through-on-hover"; + pub fn init() -> TauriPlugin { - PluginBuilder::new("proj-airi-tauri-plugin-window-pass-through-on-hover") + let builder = tauri_specta::Builder::::new() + .plugin_name(PLUGIN_NAME) + // ↓ HACKY WARNING + // ↓ Below is a modified version from the expansion of `tauri_specta::collect_commands!`, + // ↓ as the original macro does not accept a mix of commands with and without `#[specta::specta]` + .commands(tauri_specta::internal::command( + tauri::generate_handler![ + start_monitor, + stop_monitor, + start_pass_through, + stop_pass_through + ], + specta::function::collect_functions![ + start_pass_through::, + // ^^^^^^^^^^ + // TODO: We have to specify the runtime type here. This is a known issue: + // - https://github.com/specta-rs/tauri-specta/issues/70 + // - https://github.com/specta-rs/tauri-specta/issues/162 + stop_pass_through::, + ] + )); + + #[cfg(debug_assertions)] + builder + .export( + Typescript::default(), + "../src/commands/bindings/window-pass-through-on-hover.ts", + ) + .expect("Failed to export typescript bindings"); + + PluginBuilder::new(PLUGIN_NAME) .setup(|app, _| { app.manage(WindowClickThroughState::default()); Ok(()) }) - .invoke_handler(tauri::generate_handler![ - start_monitor, - stop_monitor, - start_pass_through, - stop_pass_through, - ]) + .invoke_handler(builder.invoke_handler()) .build() } diff --git a/apps/stage-tamagotchi/src-tauri/src/plugins/window_router_link/mod.rs b/apps/stage-tamagotchi/src-tauri/src/plugins/window_router_link/mod.rs index 490fce8e8..5156ea8e9 100644 --- a/apps/stage-tamagotchi/src-tauri/src/plugins/window_router_link/mod.rs +++ b/apps/stage-tamagotchi/src-tauri/src/plugins/window_router_link/mod.rs @@ -1,3 +1,5 @@ +#[cfg(debug_assertions)] +use specta_typescript::Typescript; use tauri::{ Manager, Runtime, @@ -7,27 +9,33 @@ use tauri::{ use crate::app::windows::{chat, settings}; #[tauri::command] +#[specta::specta] async fn go( window: tauri::Window, route: String, - window_label: String, + // ↓ #[specta(optional)] is not available in parameters. + // ↓ This will be `string | null` in the generated TypeScript code + window_label: Option, ) -> std::result::Result<(), String> { let app = window.app_handle(); - let target_window = match window_label.as_str() { - "chat" => match app.get_webview_window("chat") { + let target_window = match window_label.as_deref() { + Some("chat") => match app.get_webview_window("chat") { Some(window) => window, None => { chat::new_chat_window(app).map_err(|e| format!("Failed to create chat window: {}", e))? }, }, - "settings" => match app.get_webview_window("settings") { + Some("settings") => match app.get_webview_window("settings") { Some(window) => window, None => settings::new_settings_window(app) .map_err(|e| format!("Failed to create settings window: {}", e))?, }, - _ => { - return Err(format!("Unknown window label: {}", window_label)); + Some(label) => { + return Err(format!("Unknown window label: {}", label)); + }, + None => { + return Err("Missing window label".into()); }, }; @@ -43,9 +51,23 @@ async fn go( Ok(()) } +const PLUGIN_NAME: &str = "proj-airi-tauri-plugin-window-router-link"; + pub fn init() -> TauriPlugin { - Builder::new("proj-airi-tauri-plugin-window-router-link") - .invoke_handler(tauri::generate_handler![go]) + let builder = tauri_specta::Builder::::new() + .plugin_name(PLUGIN_NAME) + .commands(tauri_specta::collect_commands![go::]); + + #[cfg(debug_assertions)] + builder + .export( + Typescript::default(), + "../src/commands/bindings/window-router-link.ts", + ) + .expect("Failed to export typescript bindings"); + + Builder::new(PLUGIN_NAME) + .invoke_handler(builder.invoke_handler()) .setup(|_, _| Ok(())) .build() } diff --git a/apps/stage-tamagotchi/src/commands/bindings/window-pass-through-on-hover.ts b/apps/stage-tamagotchi/src/commands/bindings/window-pass-through-on-hover.ts new file mode 100644 index 000000000..a9f465f55 --- /dev/null +++ b/apps/stage-tamagotchi/src/commands/bindings/window-pass-through-on-hover.ts @@ -0,0 +1,96 @@ + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async startPassThrough() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|start_pass_through") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async stopPassThrough() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|stop_pass_through") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +} +} + +/** user-defined events **/ + + + +/** user-defined constants **/ + + + +/** user-defined types **/ + + + +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/apps/stage-tamagotchi/src/commands/bindings/window-router-link.ts b/apps/stage-tamagotchi/src/commands/bindings/window-router-link.ts new file mode 100644 index 000000000..a6ab9f939 --- /dev/null +++ b/apps/stage-tamagotchi/src/commands/bindings/window-router-link.ts @@ -0,0 +1,88 @@ + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async go(route: string, windowLabel: string | null) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:proj-airi-tauri-plugin-window-router-link|go", { route, windowLabel }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +} +} + +/** user-defined events **/ + + + +/** user-defined constants **/ + + + +/** user-defined types **/ + + + +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/apps/stage-tamagotchi/src/commands/index.ts b/apps/stage-tamagotchi/src/commands/index.ts new file mode 100644 index 000000000..b2cb6b810 --- /dev/null +++ b/apps/stage-tamagotchi/src/commands/index.ts @@ -0,0 +1,2 @@ +export * as WindowPassThroughOnHover from './bindings/window-pass-through-on-hover' +export * as WindowRouterLink from './bindings/window-router-link' diff --git a/apps/stage-tamagotchi/src/components/Tauri/WindowRouterLink.vue b/apps/stage-tamagotchi/src/components/Tauri/WindowRouterLink.vue index fc4684bb6..cd3b4ddf6 100644 --- a/apps/stage-tamagotchi/src/components/Tauri/WindowRouterLink.vue +++ b/apps/stage-tamagotchi/src/components/Tauri/WindowRouterLink.vue @@ -1,18 +1,13 @@ diff --git a/apps/stage-tamagotchi/src/tauri/invoke.ts b/apps/stage-tamagotchi/src/tauri/invoke.ts index ae888a073..217a133e7 100644 --- a/apps/stage-tamagotchi/src/tauri/invoke.ts +++ b/apps/stage-tamagotchi/src/tauri/invoke.ts @@ -18,15 +18,8 @@ export interface InvokeMethods { // Plugin - Window Pass through on hover 'plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|start_monitor': { args: undefined, options: undefined, returns: void } 'plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|stop_monitor': { args: undefined, options: undefined, returns: void } - 'plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|start_pass_through': { args: undefined, options: undefined, returns: void } - 'plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|stop_pass_through': { args: undefined, options: undefined, returns: void } // Plugin - WindowRouterLink - 'plugin:proj-airi-tauri-plugin-window-router-link|go': { - args: { route: string, windowLabel?: string } | undefined - options: undefined - returns: void - } } export interface InvokeMethodShape { diff --git a/apps/stage-tamagotchi/src/utils/windows.ts b/apps/stage-tamagotchi/src/utils/windows.ts index 175cfccac..f3c33cf6b 100644 --- a/apps/stage-tamagotchi/src/utils/windows.ts +++ b/apps/stage-tamagotchi/src/utils/windows.ts @@ -1,11 +1,9 @@ -import { useTauriCore } from '../composables/tauri' +import { WindowPassThroughOnHover } from '../commands' export async function startClickThrough() { - const { invoke } = useTauriCore() - await invoke('plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|start_pass_through') + await WindowPassThroughOnHover.commands.startPassThrough() } export async function stopClickThrough() { - const { invoke } = useTauriCore() - await invoke('plugin:proj-airi-tauri-plugin-window-pass-through-on-hover|stop_pass_through') + await WindowPassThroughOnHover.commands.stopPassThrough() } diff --git a/cspell.config.yaml b/cspell.config.yaml index 9a279e339..af525c997 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -185,6 +185,7 @@ words: - sizecheck - smallserial - Sniglet + - specta - ssml - staticlib - stepfun diff --git a/eslint.config.ts b/eslint.config.ts index 551760011..98023ce28 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -8,6 +8,7 @@ export default await antfu( '**/assets/js/**', '**/assets/live2d/models/**', 'apps/stage-tamagotchi/out/**', + 'apps/stage-tamagotchi/src/commands/bindings/**', 'apps/stage-tamagotchi/src-tauri/**', 'crates/**', '**/drizzle/**',