diff --git a/example/src/bindings.ts b/example/src/bindings.ts index 5925f46..71edd56 100644 --- a/example/src/bindings.ts +++ b/example/src/bindings.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. export const commands = { @@ -8,26 +7,24 @@ export const commands = { * !!!! */ async helloWorld(myName: string): Promise { - return await TAURI_INVOKE("plugin:tauri-specta|hello_world", { - myName, - }); + return await TAURI_INVOKE("plugin:tauri-specta|hello_world", { myName }); }, async goodbyeWorld(): Promise { - return await TAURI_INVOKE("plugin:tauri-specta|goodbye_world"); + return await TAURI_INVOKE("plugin:tauri-specta|goodbye_world"); }, async hasError(): Promise<__Result__> { try { - return [ - await TAURI_INVOKE("plugin:tauri-specta|has_error"), - undefined, - ]; - } catch (e: any) { + return { + status: "ok", + data: await TAURI_INVOKE("plugin:tauri-specta|has_error"), + }; + } catch (e) { if (e instanceof Error) throw e; - else return [undefined, e]; + else return { status: "error", error: e as any }; } }, async someStruct(): Promise { - return await TAURI_INVOKE("plugin:tauri-specta|some_struct"); + return await TAURI_INVOKE("plugin:tauri-specta|some_struct"); }, }; @@ -39,10 +36,14 @@ export const events = __makeEvents__<{ emptyEvent: "plugin:tauri-specta:empty-event", }); +/** user-defined types **/ + export type EmptyEvent = null; export type MyStruct = { some_field: string }; export type DemoEvent = string; +/** tauri-specta globals **/ + import { invoke as TAURI_INVOKE } from "@tauri-apps/api"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; import { type WebviewWindowHandle as __WebviewWindowHandle__ } from "@tauri-apps/api/window"; @@ -59,7 +60,9 @@ type __EventObj__ = { : (payload: T) => ReturnType; }; -type __Result__ = [T, undefined] | [undefined, E]; +type __Result__ = + | { status: "ok"; data: T } + | { status: "error"; error: E }; function __makeEvents__>( mappings: Record diff --git a/src/globals.js b/src/globals.js index 4149959..ec41cc6 100644 --- a/src/globals.js +++ b/src/globals.js @@ -19,8 +19,8 @@ import * as TAURI_API_EVENT from "@tauri-apps/api/event"; */ /** - * #template T,E - * @typedef {[T, undefined] | [undefined, E]} __Result__ + * @template T,E + * @typedef { { status: "ok", data: T } | { status: "error", error: E } } __Result__ */ /** diff --git a/src/globals.ts b/src/globals.ts index 802a4c4..f2e44c3 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -14,7 +14,9 @@ type __EventObj__ = { : (payload: T) => ReturnType; }; -type __Result__ = [T, undefined] | [undefined, E]; +type __Result__ = + | { status: "ok"; data: T } + | { status: "error"; error: E }; function __makeEvents__>( mappings: Record diff --git a/src/js.rs b/src/js.rs index fc93a79..52e1a6c 100644 --- a/src/js.rs +++ b/src/js.rs @@ -1,30 +1,25 @@ use crate::{ - ts::ExportConfig, EventDataType, ExportLanguage, ItemType, NoCommands, NoEvents, DO_NOT_EDIT, + js_ts, ts::ExportConfig, EventDataType, ExportLanguage, NoCommands, NoEvents, PluginBuilder, }; use heck::ToLowerCamelCase; use indoc::formatdoc; use specta::{ functions::FunctionDataType, ts::{self, js_doc, TsExportError}, - DataType, TypeMap, + TypeMap, }; use tauri::Runtime; /// Implements [`ExportLanguage`] for JS exporting pub struct Language; -/// [`Exporter`](crate::Exporter) for JavaScript -pub type PluginBuilder = crate::PluginBuilder; - -pub fn builder() -> PluginBuilder, NoEvents> { +pub fn builder() -> PluginBuilder, NoEvents> { PluginBuilder::default() } -impl ExportLanguage for Language { - fn globals() -> String { - include_str!("./globals.js").to_string() - } +pub const GLOBALS: &str = include_str!("./globals.js"); +impl ExportLanguage for Language { /// Renders a collection of [`FunctionDataType`] into a JavaScript string. fn render_commands( commands: &[FunctionDataType], @@ -32,27 +27,16 @@ impl ExportLanguage for Language { cfg: &ExportConfig, ) -> Result { let commands = commands - .into_iter() + .iter() .map(|function| { let jsdoc = { - let ret_type = match &function.result { - DataType::Result(t) => { - let (t, e) = t.as_ref(); - - format!( - "[{}, undefined] | [undefined, {}]", - ts::datatype(&cfg.inner, t, &type_map)?, - ts::datatype(&cfg.inner, e, &type_map)? - ) - } - t => ts::datatype(&cfg.inner, t, &type_map)?, - }; + let ret_type = js_ts::handle_result(function, type_map, cfg)?; let vec = [] .into_iter() .chain(function.docs.iter().map(|s| s.to_string())) .chain(function.args.iter().flat_map(|(name, typ)| { - ts::datatype(&cfg.inner, typ, &type_map).map(|typ| { + ts::datatype(&cfg.inner, typ, type_map).map(|typ| { let name = name.to_lower_camel_case(); format!("@param {{ {typ} }} {name}") @@ -65,48 +49,13 @@ impl ExportLanguage for Language { js_doc(&vec) }; - let name_camel = function.name.to_lower_camel_case(); - - let arg_list = function - .args - .iter() - .map(|(name, _)| name.to_lower_camel_case()) - .collect::>(); - - let arg_defs = arg_list.join(", "); - - let body = { - let name = cfg - .plugin_name - .apply_as_prefix(&function.name, ItemType::Command); - - let arg_usages = arg_list - .is_empty() - .then(Default::default) - .unwrap_or_else(|| format!(", {{ {} }}", arg_list.join(", "))); - - let invoke = format!("await invoke()(\"{name}\"{arg_usages})"); - - match &function.result { - DataType::Result(_) => formatdoc!( - r#" - try {{ - return [{invoke}, undefined]; - }} catch (e) {{ - if(e instanceof Error) throw e; - else return [undefined, e]; - }}"# - ), - _ => format!("return {invoke};"), - } - }; - - Ok(formatdoc! { - r#" - {jsdoc}async {name_camel}({arg_defs}) {{ - {body} - }}"# - }) + Ok(js_ts::function( + &jsdoc, + &function.name.to_lower_camel_case(), + &js_ts::arg_names(&function.args), + None, + &js_ts::command_body(cfg, function, false), + )) }) .collect::, TsExportError>>()? .join(",\n"); @@ -128,34 +77,12 @@ impl ExportLanguage for Language { return Ok(Default::default()); } - let events_map = events - .iter() - .map(|event| { - let name_str = cfg - .plugin_name - .apply_as_prefix(&event.name, ItemType::Event); - let name_camel = event.name.to_lower_camel_case(); - - format!(r#" {name_camel}: "{name_str}""#) - }) - .collect::>() - .join(",\n"); - - let events = events - .iter() - .map(|event| { - let typ = ts::datatype(&cfg.inner, &event.typ, type_map)?; - - let name_camel = event.name.to_lower_camel_case(); - - Ok(format!(r#"{name_camel}: {typ},"#)) - }) - .collect::, TsExportError>>()?; + let (events_types, events_map) = js_ts::events_data(events, cfg, type_map)?; let events = js_doc( &[].into_iter() .chain(["@type {typeof __makeEvents__<{".to_string()]) - .chain(events) + .chain(events_types) .chain(["}>}".to_string()]) .map(Into::into) .collect::>(), @@ -178,16 +105,11 @@ impl ExportLanguage for Language { type_map: &TypeMap, cfg: &ExportConfig, ) -> Result { - let globals = Self::globals(); - - let commands = Self::render_commands(commands, &type_map, cfg)?; - let events = Self::render_events(events, &type_map, cfg)?; - let dependant_types = type_map .values() .filter_map(|v| v.as_ref()) .map(|v| { - ts::named_datatype(&cfg.inner, v, &type_map).map(|typ| { + ts::named_datatype(&cfg.inner, v, type_map).map(|typ| { let name = v.name(); js_doc(&[format!("@typedef {{ {typ} }} {name}").into()]) @@ -196,18 +118,6 @@ impl ExportLanguage for Language { .collect::, _>>() .map(|v| v.join("\n"))?; - Ok(formatdoc! { - r#" - {DO_NOT_EDIT} - - {commands} - - {events} - - {dependant_types} - - {globals} - "# - }) + js_ts::render_all_parts::(commands, events, type_map, cfg, &dependant_types, GLOBALS) } } diff --git a/src/js_ts.rs b/src/js_ts.rs new file mode 100644 index 0000000..025a6b2 --- /dev/null +++ b/src/js_ts.rs @@ -0,0 +1,172 @@ +use std::borrow::Cow; + +use heck::ToLowerCamelCase; +use indoc::formatdoc; +use specta::{ + functions::FunctionDataType, + ts::{self, TsExportError}, + DataType, TypeMap, +}; + +use crate::{ts::ExportConfig, EventDataType, ExportLanguage, ItemType}; + +pub const DO_NOT_EDIT: &str = "// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually."; + +pub fn render_all_parts( + commands: &[FunctionDataType], + events: &[EventDataType], + type_map: &TypeMap, + cfg: &ExportConfig, + dependant_types: &str, + globals: &str, +) -> Result { + let commands = T::render_commands(commands, type_map, cfg)?; + let events = T::render_events(events, type_map, cfg)?; + + Ok(formatdoc! { + r#" + {DO_NOT_EDIT} + + {commands} + + {events} + + /** user-defined types **/ + + {dependant_types} + + /** tauri-specta globals **/ + + {globals} + "# + }) +} + +pub fn arg_names(args: &[(Cow<'static, str>, DataType)]) -> Vec { + args.iter() + .map(|(name, _)| name.to_lower_camel_case()) + .collect::>() +} + +pub fn arg_usages(args: &[String]) -> Option { + (!args.is_empty()).then(|| format!("{{ {} }}", args.join(", "))) +} + +fn return_as_result_tuple(expr: &str, as_any: bool) -> String { + let as_any = as_any.then_some(" as any").unwrap_or_default(); + + formatdoc!( + r#" + try {{ + return {{ status: "ok", data: {expr} }}; + }} catch (e) {{ + if(e instanceof Error) throw e; + else return {{ status: "error", error: e {as_any} }}; + }}"# + ) +} + +pub fn maybe_return_as_result_tuple(expr: &str, typ: &DataType, as_any: bool) -> String { + match typ { + DataType::Result(_) => return_as_result_tuple(expr, as_any), + _ => format!("return {expr};"), + } +} + +pub fn function( + docs: &str, + name: &str, + args: &[String], + return_type: Option<&str>, + body: &str, +) -> String { + let args = args.join(", "); + let return_type = return_type + .map(|t| format!(": Promise<{}>", t)) + .unwrap_or_default(); + + formatdoc! { + r#" + {docs}async {name}({args}) {return_type} {{ + {body} + }}"# + } +} + +fn tauri_invoke(name: &str, arg_usages: Option) -> String { + let arg_usages = arg_usages.map(|u| format!(", {u}")).unwrap_or_default(); + + format!(r#"await TAURI_INVOKE("{name}"{arg_usages})"#) +} + +pub fn handle_result( + function: &FunctionDataType, + type_map: &TypeMap, + cfg: &ExportConfig, +) -> Result { + Ok(match &function.result { + DataType::Result(t) => { + let (t, e) = t.as_ref(); + + format!( + "__Result__<{}, {}>", + ts::datatype(&cfg.inner, t, type_map)?, + ts::datatype(&cfg.inner, e, type_map)? + ) + } + t => ts::datatype(&cfg.inner, t, type_map)?, + }) +} + +pub fn command_body(cfg: &ExportConfig, function: &FunctionDataType, as_any: bool) -> String { + let name = cfg + .plugin_name + .apply_as_prefix(&function.name, ItemType::Command); + + maybe_return_as_result_tuple( + &tauri_invoke(&name, arg_usages(&arg_names(&function.args))), + &function.result, + as_any, + ) +} + +pub fn events_map(events: &[EventDataType], cfg: &ExportConfig) -> String { + events + .iter() + .map(|event| { + let name_str = cfg.plugin_name.apply_as_prefix(event.name, ItemType::Event); + let name_camel = event.name.to_lower_camel_case(); + + format!(r#"{name_camel}: "{name_str}""#) + }) + .collect::>() + .join(",\n") +} + +pub fn events_types( + events: &[EventDataType], + cfg: &ExportConfig, + type_map: &TypeMap, +) -> Result, TsExportError> { + events + .iter() + .map(|event| { + let name_camel = event.name.to_lower_camel_case(); + + let typ = ts::datatype(&cfg.inner, &event.typ, type_map)?; + + Ok(format!(r#"{name_camel}: {typ}"#)) + }) + .collect() +} + +pub fn events_data( + events: &[EventDataType], + cfg: &ExportConfig, + type_map: &TypeMap, +) -> Result<(Vec, String), TsExportError> { + Ok(( + events_types(events, cfg, type_map)?, + events_map(events, cfg), + )) +} diff --git a/src/lib.rs b/src/lib.rs index 47509e0..c0e0a60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,7 +94,6 @@ #![cfg_attr(docsrs, feature(doc_cfg))] use std::{ - borrow::Cow, fs::{self, File}, io::Write, marker::PhantomData, @@ -118,6 +117,7 @@ pub mod js; pub mod ts; mod event; +mod js_ts; pub use event::*; @@ -139,8 +139,6 @@ macro_rules! collect_commands { }}; } -pub(crate) const DO_NOT_EDIT: &str = "// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually."; - pub(crate) const CRINGE_ESLINT_DISABLE: &str = "/* eslint-disable */ "; @@ -150,9 +148,6 @@ pub(crate) const CRINGE_ESLINT_DISABLE: &str = "/* eslint-disable */ /// A set of functions that produce language-specific code pub trait ExportLanguage: 'static { - /// Type definitions and constants that the generated functions rely on - fn globals() -> String; - fn render_events( events: &[EventDataType], type_map: &TypeMap, diff --git a/src/ts.rs b/src/ts.rs index bfeef80..e942bf6 100644 --- a/src/ts.rs +++ b/src/ts.rs @@ -1,33 +1,28 @@ use std::{borrow::Cow, path::PathBuf}; use crate::{ - EventDataType, ExportLanguage, ItemType, NoCommands, NoEvents, PluginName, - CRINGE_ESLINT_DISABLE, DO_NOT_EDIT, + js_ts, EventDataType, ExportLanguage, NoCommands, NoEvents, PluginBuilder, PluginName, + CRINGE_ESLINT_DISABLE, }; use heck::ToLowerCamelCase; use indoc::formatdoc; use specta::{ functions::FunctionDataType, ts::{self, TsExportError}, - DataType, TypeMap, + TypeMap, }; use tauri::Runtime; /// Implements [`ExportLanguage`] for TypeScript exporting pub struct Language; -/// [`Exporter`](crate::Exporter) for TypeScript -pub type PluginBuilder = crate::PluginBuilder; - -pub fn builder() -> PluginBuilder, NoEvents> { +pub fn builder() -> PluginBuilder, NoEvents> { PluginBuilder::default() } -impl ExportLanguage for Language { - fn globals() -> String { - include_str!("./globals.ts").to_string() - } +pub const GLOBALS: &str = include_str!("./globals.ts"); +impl ExportLanguage for Language { /// Renders a collection of [`FunctionDataType`] into a TypeScript string. fn render_commands( commands: &[FunctionDataType], @@ -37,10 +32,6 @@ impl ExportLanguage for Language { let commands = commands .iter() .map(|function| { - let docs = specta::ts::js_doc(&function.docs); - - let name_camel = function.name.to_lower_camel_case(); - let arg_defs = function .args .iter() @@ -48,67 +39,16 @@ impl ExportLanguage for Language { ts::datatype(&cfg.inner, typ, type_map) .map(|ty| format!("{}: {}", name.to_lower_camel_case(), ty)) }) - .collect::, _>>()? - .join(", "); - - let ok_type = match &function.result { - DataType::Result(t) => { - let (t, _) = t.as_ref(); - - ts::datatype(&cfg.inner, t, type_map)? - } - t => ts::datatype(&cfg.inner, t, type_map)?, - }; - - let ret_type = match &function.result { - DataType::Result(t) => { - let (_, e) = t.as_ref(); - - format!( - "__Result__<{ok_type}, {}>", - ts::datatype(&cfg.inner, e, type_map)? - ) - } - _ => ok_type.clone(), - }; - - let body = { - let name = cfg - .plugin_name - .apply_as_prefix(&function.name, ItemType::Command); - - let arg_usages = function - .args - .iter() - .map(|(name, _)| name.to_lower_camel_case()) - .collect::>(); - - let arg_usages = arg_usages - .is_empty() - .then(Default::default) - .unwrap_or_else(|| format!(", {{ {} }}", arg_usages.join(","))); - - let invoke = format!("await TAURI_INVOKE<{ok_type}>(\"{name}\"{arg_usages})"); - - match &function.result { - DataType::Result(_) => formatdoc!( - r#" - try {{ - return [{invoke}, undefined]; - }} catch (e: any) {{ - if(e instanceof Error) throw e; - else return [undefined, e]; - }}"# - ), - _ => format!("return {invoke};"), - } - }; - - Ok(formatdoc!( - r#" - {docs}async {name_camel}({arg_defs}): Promise<{ret_type}> {{ - {body} - }}"# + .collect::, _>>()?; + + let ret_type = js_ts::handle_result(function, type_map, cfg)?; + + Ok(js_ts::function( + &specta::ts::js_doc(&function.docs), + &function.name.to_lower_camel_case(), + &arg_defs, + Some(&ret_type), + &js_ts::command_body(cfg, function, true), )) }) .collect::, TsExportError>>()? @@ -131,33 +71,14 @@ impl ExportLanguage for Language { return Ok(Default::default()); } - let events_map = events - .iter() - .map(|event| { - let name_str = cfg.plugin_name.apply_as_prefix(event.name, ItemType::Event); - let name_camel = event.name.to_lower_camel_case(); - - format!(r#"{name_camel}: "{name_str}""#) - }) - .collect::>() - .join(",\n"); - - let events = events - .iter() - .map(|event| { - let name_camel = event.name.to_lower_camel_case(); + let (events_types, events_map) = js_ts::events_data(events, cfg, type_map)?; - let typ = ts::datatype(&cfg.inner, &event.typ, type_map)?; - - Ok(format!(r#" {name_camel}: {typ}"#)) - }) - .collect::, TsExportError>>()? - .join(",\n"); + let events_types = events_types.join(",\n"); Ok(formatdoc! { r#" export const events = __makeEvents__<{{ - {events} + {events_types} }}>({{ {events_map} }})"# @@ -170,11 +91,6 @@ impl ExportLanguage for Language { type_map: &TypeMap, cfg: &ExportConfig, ) -> Result { - let globals = Self::globals(); - - let commands = Self::render_commands(commands, type_map, cfg)?; - let events = Self::render_events(events, type_map, cfg)?; - let dependant_types = type_map .values() .filter_map(|v| v.as_ref()) @@ -182,19 +98,7 @@ impl ExportLanguage for Language { .collect::, _>>() .map(|v| v.join("\n"))?; - Ok(formatdoc! { - r#" - {DO_NOT_EDIT} - - {commands} - - {events} - - {dependant_types} - - {globals} - "# - }) + js_ts::render_all_parts::(commands, events, type_map, cfg, &dependant_types, GLOBALS) } }