From c976b79c7d2026897fa6595405358b70ec8a5d44 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:34:55 +0200 Subject: [PATCH] chore(ipc): bootstrap tauri-specta generator (1 command, no cutover) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stand up the FE<->BE contract pipeline end to end with near-zero surface: a tauri_specta::Builder collects one proven command (greet) and exports typed TS bindings to src/generated/bindings.ts. The existing generate_handler! stays the live invoke handler — no FE change yet. Safety: the export runs only under #[cfg(debug_assertions)] (debug launch) and a headless test, never in a release build, so a specta RC break can never block a release cargo build; the committed bindings.ts is plain TypeScript for tsc. The snapshot is committed (gitignore exception) so CI diffs catch contract drift. Pinned deps: specta / tauri-specta =2.0.0-rc.25, specta-typescript =0.0.12 (the latest mutually-consistent set; rc.21 + specta-typescript 0.0.9 no longer resolve). Grow collect_commands! crate-by-crate next, starting with psysonic-library. --- .gitignore | 8 +- src-tauri/Cargo.lock | 92 ++++++++++++++++++++++ src-tauri/Cargo.toml | 3 + src-tauri/src/lib.rs | 44 +++++++++++ src-tauri/src/lib_commands/app_api/core.rs | 1 + src-tauri/src/lib_commands/app_api/mod.rs | 4 +- src/generated/bindings.ts | 9 +++ 7 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 src/generated/bindings.ts diff --git a/.gitignore b/.gitignore index 70310b00..89e584b6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,8 +37,12 @@ src-tauri/lcov.info # Frontend test coverage coverage/ -# Generated at build/test/dev time (scripts/generate-release-notes-bundle.mjs) -src/generated/ +# Generated at build/test/dev time (scripts/generate-release-notes-bundle.mjs). +# Ignore the contents (not the dir) so the committed tauri-specta FE↔BE contract +# snapshot below can be re-included — a `!` exception cannot escape an ignored dir. +src/generated/* +# ...except the committed tauri-specta contract snapshot, so CI diffs catch drift. +!src/generated/bindings.ts # Documentation CLAUDE.md diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e7fb2683..a52103be 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "adler2" version = "2.0.1" @@ -4140,6 +4146,8 @@ dependencies = [ "serde", "serde_json", "souvlaki", + "specta", + "specta-typescript", "symphonia", "symphonia-adapter-libopus", "sysinfo", @@ -4154,6 +4162,7 @@ dependencies = [ "tauri-plugin-store", "tauri-plugin-updater", "tauri-plugin-window-state", + "tauri-specta", "thread-priority", "tokio", "url", @@ -5381,6 +5390,57 @@ dependencies = [ "zvariant 3.15.2", ] +[[package]] +name = "specta" +version = "2.0.0-rc.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50" +dependencies = [ + "paste", + "rustc_version", + "specta-macros", +] + +[[package]] +name = "specta-macros" +version = "2.0.0-rc.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "specta-serde" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029" +dependencies = [ + "specta", + "specta-macros", +] + +[[package]] +name = "specta-typescript" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0" +dependencies = [ + "specta", +] + +[[package]] +name = "specta-util" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502" +dependencies = [ + "specta", +] + [[package]] name = "spin" version = "0.12.0" @@ -5821,6 +5881,7 @@ dependencies = [ "serde_json", "serde_repr", "serialize-to-javascript", + "specta", "swift-rs", "tauri-build", "tauri-macros", @@ -6133,6 +6194,37 @@ dependencies = [ "wry", ] +[[package]] +name = "tauri-specta" +version = "2.0.0-rc.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2" +dependencies = [ + "heck 0.5.0", + "serde", + "serde_json", + "specta", + "specta-serde", + "specta-typescript", + "specta-util", + "tauri", + "tauri-specta-macros", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-specta-macros" +version = "2.0.0-rc.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012" +dependencies = [ + "darling", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tauri-utils" version = "2.9.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 09a1659d..bf821611 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -44,6 +44,9 @@ psysonic-library = { path = "crates/psysonic-library" } psysonic-syncfs = { path = "crates/psysonic-syncfs" } psysonic-integration = { path = "crates/psysonic-integration" } tauri = { version = "2", features = ["protocol-asset", "tray-icon", "image-png"] } +specta = "=2.0.0-rc.25" +specta-typescript = "=0.0.12" +tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] } tauri-plugin-single-instance = "2" tauri-plugin-shell = "2" tauri-plugin-global-shortcut = "2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0c3bb4c8..84b1273a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -78,7 +78,39 @@ fn set_app_user_model_id() { } } +/// FE↔BE contract via tauri-specta. The builder collects `#[specta::specta]` +/// commands and exports typed TS bindings; the existing `generate_handler!` stays +/// the live invoke handler (no cutover yet). Export runs only in debug builds / +/// tests, so a specta RC break can never block a release `cargo build` — the +/// committed `bindings.ts` is plain TypeScript for `tsc`. Grow `collect_commands!` +/// crate-by-crate (see the specta-contract plan). +fn specta_builder() -> tauri_specta::Builder { + tauri_specta::Builder::::new() + .commands(tauri_specta::collect_commands![ + crate::lib_commands::app_api::core::greet + ]) +} + +/// TS exporter config. Kept as a single seam so the exporter options (header / +/// per-type BigInt-style handling for i64 DTOs) are configured in one place as +/// commands are added crate-by-crate. +fn bindings_exporter() -> specta_typescript::Typescript { + specta_typescript::Typescript::default() +} + +/// Regenerate the committed bindings on a debug launch (matches the dev workflow; +/// the CI freshness gate runs the equivalent export in a test — see `specta_export`). +#[cfg(debug_assertions)] +fn export_specta_bindings() { + specta_builder() + .export(bindings_exporter(), "../src/generated/bindings.ts") + .expect("failed to export typescript bindings"); +} + pub fn run() { + #[cfg(debug_assertions)] + export_specta_bindings(); + // Windows: bind this process to an explicit AppUserModelID before any window // or the SMTC media controls are created, so the OS can resolve the app // name/icon for taskbar grouping and the media tile (#1102 follow-up: the @@ -922,3 +954,15 @@ pub fn run() { .run(tauri::generate_context!()) .expect("error while running Psysonic"); } + +#[cfg(test)] +mod specta_export { + // Headless generator + freshness seed: runs the same export as a debug launch + // without a GUI, so `cargo test` regenerates `bindings.ts` and CI can diff it. + #[test] + fn bindings_are_exportable() { + super::specta_builder() + .export(super::bindings_exporter(), "../src/generated/bindings.ts") + .expect("failed to export typescript bindings"); + } +} diff --git a/src-tauri/src/lib_commands/app_api/core.rs b/src-tauri/src/lib_commands/app_api/core.rs index 3312e1e9..9f7a4f7f 100644 --- a/src-tauri/src/lib_commands/app_api/core.rs +++ b/src-tauri/src/lib_commands/app_api/core.rs @@ -5,6 +5,7 @@ use crate::runtime_subsonic_wire_user_agent; use crate::analysis_cache; #[tauri::command] +#[specta::specta] pub(crate) fn greet(name: &str) -> String { format!("Hello, {}!", name) } diff --git a/src-tauri/src/lib_commands/app_api/mod.rs b/src-tauri/src/lib_commands/app_api/mod.rs index de2dbd3e..b0157e66 100644 --- a/src-tauri/src/lib_commands/app_api/mod.rs +++ b/src-tauri/src/lib_commands/app_api/mod.rs @@ -1,6 +1,8 @@ mod backup; mod cli_bridge; -mod core; +// `pub(crate)` so tauri-specta's `collect_commands!` can reach the `#[specta::specta]` +// helper macro by full path (a `pub use` of the fn does not carry the macro). +pub(crate) mod core; mod integration; mod migration; mod network; diff --git a/src/generated/bindings.ts b/src/generated/bindings.ts new file mode 100644 index 00000000..bc1167a7 --- /dev/null +++ b/src/generated/bindings.ts @@ -0,0 +1,9 @@ +// This file has been generated by Tauri Specta. Do not edit this file manually. + +import { invoke as __TAURI_INVOKE } from "@tauri-apps/api/core"; + +/** Commands */ +export const commands = { + greet: (name: string) => __TAURI_INVOKE("greet", { name }), +}; +