Explorar el Código

feat(tauri.js) improve api module with type defs for each API fu… (#495)

* feat(tauri.js) improve api module with type defs for each API function

* chore(tauri) move endpoint specific modules

* refactor(tauri) move tcp mod to tauri_api

* feat(tauri) improve readDir signature, API features as kebab-case names

* fix(tauri) make event's payload optional

* feat(tauri) if invoke_handler fails, console.error the error message

* chore(api) improve JSDoc

* chore(tauri.js) update template

* chore(tauri) delete empty mod

* fix(tauri) tests and example with latest API signature
Lucas Fernandes Nogueira hace 5 años
padre
commit
b95319bd74

+ 34 - 0
cli/tauri.js/api/event.js

@@ -0,0 +1,34 @@
+import tauri from './tauri'
+
+/**
+ * The event handler callback
+ * @callback eventCallback
+ * @param {object} event
+ * @param {string} event.type
+ * @param {any} [event.payload]
+ */
+
+/**
+ * listen to an event from the backend
+ * 
+ * @param {string} event the event name
+ * @param {eventCallback} handler the event handler callback
+ */
+function listen (event, handler) {
+  tauri.listen(event, handler)
+}
+
+/**
+ * emits an event to the backend
+ *
+ * @param {string} event the event name
+ * @param {string} [payload] the event payload
+ */
+function emit (event, payload) {
+  tauri.emit(event, payload)
+}
+
+export {
+  listen,
+  emit
+}

+ 59 - 0
cli/tauri.js/api/fs.js

@@ -0,0 +1,59 @@
+import tauri from './tauri'
+
+/**
+ * reads a file as text
+ *
+ * @param {string} filePath path to the file
+ * @return {Promise<string>}
+ */
+function readTextFile (filePath) {
+  return tauri.readTextFile(filePath)
+}
+
+/**
+ * reads a file as binary
+ *
+ * @param {string} filePath path to the file
+ * @return {Promise<int[]>}
+ */
+function readBinaryFile (filePath) {
+  return tauri.readBinaryFile(filePath)
+}
+
+/**
+ * writes a text file
+ *
+ * @param {object} file
+ * @param {string} file.path path of the file
+ * @param {string} file.contents contents of the file
+ * @return {Promise<void>}
+ */
+function writeFile (file) {
+  return tauri.writeFile(file)
+}
+
+/**
+ * @typedef {object} FileEntry
+ * @property {string} path
+ * @property {boolean} is_dir
+ * @property {string} name
+ */
+
+/**
+ * list directory files
+ *
+ * @param {string} dir path to the directory to read
+ * @param {object} [options] configuration object
+ * @param {boolean} [options.recursive] whether to list dirs recursively or not
+ * @return {Promise<FileEntry[]>}
+ */
+function readDir (dir, options = {}) {
+  return tauri.readDir(dir, options)
+}
+
+export {
+  readTextFile,
+  readBinaryFile,
+  writeFile,
+  readDir
+}

+ 3 - 0
cli/tauri.js/api/index.js

@@ -0,0 +1,3 @@
+import tauri from './tauri'
+
+export default tauri

+ 16 - 0
cli/tauri.js/api/process.js

@@ -0,0 +1,16 @@
+import tauri from './tauri'
+
+/**
+ * spawns a process
+ *
+ * @param {string} command the name of the cmd to execute e.g. 'mkdir' or 'node'
+ * @param {(string[]|string)} [args] command args
+ * @return {Promise<string>} promise resolving to the stdout text
+ */
+function execute (command, args) {
+  return tauri.execute(command, args)
+}
+
+export {
+  execute
+}

+ 2 - 2
cli/tauri.js/api.js → cli/tauri.js/api/tauri.js

@@ -2,7 +2,7 @@ const cache = {}
 let initialized = false
 
 const proxy = new Proxy({
-  __consume () {
+  __consume() {
     for (const key in cache) {
       if (key in window.tauri) {
         const queue = cache[key]
@@ -23,7 +23,7 @@ const proxy = new Proxy({
     initialized = true
   }
 }, {
-  get (obj, prop) {
+  get(obj, prop) {
     if (prop === '__consume') {
       return obj[prop]
     }

+ 24 - 0
cli/tauri.js/api/window.js

@@ -0,0 +1,24 @@
+import tauri from './tauri'
+
+/**
+ * sets the window title
+ *
+ * @param {string} title the new title
+ */
+function setTitle (title) {
+  tauri.setTitle(title)
+}
+
+/**
+ * opens an URL on the user default browser
+ *
+ * @param {string} url the URL to open
+ */
+function open (url) {
+  tauri.open(url)
+}
+
+export {
+  setTitle,
+  open
+}

+ 6 - 1
cli/tauri.js/src/runner.ts

@@ -370,10 +370,15 @@ class Runner {
     if (cfg.tauri.whitelist.all) {
       tomlFeatures.push('all-api')
     } else {
+      const toKebabCase = (value: string): string => {
+        return value.replace(/([a-z])([A-Z])/g, "$1-$2")
+          .replace(/\s+/g, '-')
+          .toLowerCase()
+      }
       const whitelist = Object.keys(cfg.tauri.whitelist).filter(
         w => cfg.tauri.whitelist[String(w)] === true
       )
-      tomlFeatures.push(...whitelist)
+      tomlFeatures.push(...whitelist.map(toKebabCase))
     }
 
     if (cfg.tauri.edge.active) {

+ 4 - 1
cli/tauri.js/templates/src-tauri/src/main.rs

@@ -10,7 +10,9 @@ fn main() {
     .invoke_handler(|_webview, arg| {
       use cmd::Cmd::*;
       match serde_json::from_str(arg) {
-        Err(_) => {}
+        Err(e) => {
+          Err(e.to_string())
+        }
         Ok(command) => {
           match command {
             // definitions for your custom commands from Cmd here
@@ -19,6 +21,7 @@ fn main() {
               println!("{}", argument);
             }
           }
+          Ok(())
         }
       }
     })

+ 1 - 21
cli/tauri.js/templates/tauri.js

@@ -153,7 +153,7 @@ window.tauri = {
       this.invoke({
         cmd: 'emit',
         event: evt,
-        payload: payload || ''
+        payload: payload
       });
     <% } else { %>
       <% if (ctx.dev) { %>
@@ -398,26 +398,6 @@ window.tauri = {
         <% } %>
   },
 
-bridge: function bridge(command, payload) {
-    <% if (tauri.whitelist.bridge === true || tauri.whitelist.all === true) { %>
-
-    if (_typeof(payload) === 'object') {
-      Object.freeze(payload);
-    }
-
-    return this.promisified({
-      cmd: 'bridge',
-      command: command,
-      payload: _typeof(payload) === 'object' ? [payload] : payload
-    });
-    <% } else { %>
-      <% if (ctx.dev) { %>
-          return __whitelistWarning('bridge')
-        <% } %>
-            return __reject()
-      <% } %>
-  },
-
 loadAsset: function loadAsset(assetName, assetType) {
   return this.promisified({
     cmd: 'loadAsset',

+ 1 - 0
tauri-api/Cargo.toml

@@ -21,6 +21,7 @@ either = "1.5.3"
 tar = "0.4"
 flate2 = "1"
 error-chain = "0.12"
+rand = "0.7"
 tauri-utils = {version = "0.4", path = "../tauri-utils"}
 
 [dev-dependencies]

+ 1 - 0
tauri-api/src/lib.rs

@@ -8,6 +8,7 @@ pub mod dir;
 pub mod file;
 pub mod rpc;
 pub mod version;
+pub mod tcp;
 
 pub use tauri_utils::*;
 

+ 0 - 0
tauri/src/tcp.rs → tauri-api/src/tcp.rs


+ 5 - 7
tauri/Cargo.toml

@@ -12,7 +12,6 @@ exclude = ["test/fixture/**"]
 [dependencies]
 serde_json = "1.0"
 serde = { version = "1.0", features = ["derive"] }
-rand = "0.7"
 web-view = "0.6.0"
 tauri_includedir = "0.5.0"
 phf = "0.8.0"
@@ -41,12 +40,11 @@ dev-server = []
 embedded-server = []
 no-server = []
 all-api = []
-readTextFile = []
-readBinaryFile = []
-writeFile = []
-listFiles = []
-listDirs = []
-setTitle = []
+read-text-file = []
+read-binary-file = []
+write-file = []
+read-dir = []
+set-title = []
 execute = []
 open = []
 event = []

+ 6 - 3
tauri/examples/communication/src-tauri/src/main.rs

@@ -17,7 +17,7 @@ fn main() {
     .setup(|webview, _source| {
       let handle = webview.handle();
       tauri::event::listen(String::from("js-event"), move |msg| {
-        println!("got js-event with message '{}'", msg);
+        println!("got js-event with message '{:?}'", msg);
         let reply = Reply {
           data: "something else".to_string(),
         };
@@ -25,14 +25,16 @@ fn main() {
         tauri::event::emit(
           &handle,
           String::from("rust-event"),
-          serde_json::to_string(&reply).unwrap(),
+          Some(serde_json::to_string(&reply).unwrap()),
         );
       });
     })
     .invoke_handler(|_webview, arg| {
       use cmd::Cmd::*;
       match serde_json::from_str(arg) {
-        Err(_) => {}
+        Err(e) => {
+          Err(e.to_string())
+        }
         Ok(command) => {
           match command {
             LogOperation { event, payload } => {
@@ -56,6 +58,7 @@ fn main() {
               )
             },
           }
+          Ok(())
         }
       }
     })

+ 7 - 4
tauri/src/app.rs

@@ -2,7 +2,7 @@ use web_view::WebView;
 
 mod runner;
 
-type InvokeHandler = Box<dyn FnMut(&mut WebView<'_, ()>, &str)>;
+type InvokeHandler = Box<dyn FnMut(&mut WebView<'_, ()>, &str) -> Result<(), String>>;
 type Setup = Box<dyn FnMut(&mut WebView<'_, ()>, String)>;
 
 pub struct App {
@@ -16,9 +16,12 @@ impl App {
     runner::run(&mut self).expect("Failed to build webview");
   }
 
-  pub(crate) fn run_invoke_handler(&mut self, webview: &mut WebView<'_, ()>, arg: &str) {
+  pub(crate) fn run_invoke_handler(&mut self, webview: &mut WebView<'_, ()>, arg: &str) -> Result<bool, String> {
     if let Some(ref mut invoke_handler) = self.invoke_handler {
-      invoke_handler(webview, arg);
+      invoke_handler(webview, arg)
+        .map(|_| true)
+    } else {
+      Ok(false)
     }
   }
 
@@ -49,7 +52,7 @@ impl AppBuilder {
     }
   }
 
-  pub fn invoke_handler<F: FnMut(&mut WebView<'_, ()>, &str) + 'static>(
+  pub fn invoke_handler<F: FnMut(&mut WebView<'_, ()>, &str) -> Result<(), String> + 'static>(
     mut self,
     invoke_handler: F,
   ) -> Self {

+ 30 - 4
tauri/src/app/runner.rs

@@ -6,7 +6,7 @@ use web_view::{builder, Content, WebView};
 use super::App;
 use crate::config::{get, Config};
 #[cfg(feature = "embedded-server")]
-use crate::tcp::{get_available_port, port_is_available};
+use crate::api::tcp::{get_available_port, port_is_available};
 
 // JavaScript string literal
 const JS_STRING: &str = r#"
@@ -218,9 +218,27 @@ fn build_webview(
           Content::Url(ref url) => url,
         };
         webview.eval(&format!(r#"window.location.href = "{}""#, content_href))?;
-      } else if let Ok(b) = crate::endpoints::handle(webview, arg) {
-        if !b {
-          application.run_invoke_handler(webview, arg);
+      } else {
+        let handler_error;
+        if let Err(tauri_handle_error) = crate::endpoints::handle(webview, arg) {
+          let tauri_handle_error_str = tauri_handle_error.to_string();
+          if tauri_handle_error_str.contains("unknown variant") {
+            let handled_by_app = application.run_invoke_handler(webview, arg);
+            handler_error = if let Err(e) = handled_by_app {
+              Some(e.replace("'", "\\'"))
+            } else {
+              let handled = handled_by_app.expect("failed to check if the invoke was handled");
+              if handled { None } else { Some(tauri_handle_error_str) }
+            };
+          } else {
+            handler_error = Some(tauri_handle_error_str);
+          }
+
+          if let Some(handler_error_message) = handler_error {
+            webview.eval(
+              &get_api_error_message(arg, handler_error_message)
+            )?;
+          }
         }
       }
 
@@ -243,6 +261,14 @@ fn build_webview(
   Ok(webview)
 }
 
+fn get_api_error_message(arg: &str, handler_error_message: String) -> String {
+  format!(
+    r#"console.error('failed to match a command for {}, {}')"#, 
+    arg.replace("'", "\\'"),
+    handler_error_message
+  )
+}
+
 #[cfg(test)]
 mod test {
   use proptest::prelude::*;

+ 18 - 22
tauri/src/endpoints.rs

@@ -1,14 +1,17 @@
 mod cmd;
+mod salt;
+#[allow(dead_code)]
+mod file_system;
 
 #[cfg(not(any(feature = "dev-server", feature = "embedded-server")))]
 use std::path::PathBuf;
 use web_view::WebView;
 
 #[allow(unused_variables)]
-pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> crate::Result<bool> {
+pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> crate::Result<()> {
   use cmd::Cmd::*;
   match serde_json::from_str(arg) {
-    Err(_) => Ok(false),
+    Err(e) => Err(crate::Error::from(e.to_string())),
     Ok(command) => {
       match command {
         Init {} => {
@@ -20,48 +23,41 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
             event_init = event_init
           ))?;
         }
-        #[cfg(any(feature = "all-api", feature = "readTextFile"))]
+        #[cfg(any(feature = "all-api", feature = "read-text-file"))]
         ReadTextFile {
           path,
           callback,
           error,
         } => {
-          crate::file_system::read_text_file(webview, path, callback, error);
+          file_system::read_text_file(webview, path, callback, error);
         }
-        #[cfg(any(feature = "all-api", feature = "readBinaryFile"))]
+        #[cfg(any(feature = "all-api", feature = "read-binary-file"))]
         ReadBinaryFile {
           path,
           callback,
           error,
         } => {
-          crate::file_system::read_binary_file(webview, path, callback, error);
+          file_system::read_binary_file(webview, path, callback, error);
         }
-        #[cfg(any(feature = "all-api", feature = "writeFile"))]
+        #[cfg(any(feature = "all-api", feature = "write-file"))]
         WriteFile {
           file,
           contents,
           callback,
           error,
         } => {
-          crate::file_system::write_file(webview, file, contents, callback, error);
+          file_system::write_file(webview, file, contents, callback, error);
         }
-        #[cfg(any(feature = "all-api", feature = "listDirs"))]
-        ListDirs {
+        #[cfg(any(feature = "all-api", feature = "read-dir"))]
+        ReadDir {
           path,
+          options,
           callback,
           error,
         } => {
-          crate::file_system::list_dirs(webview, path, callback, error);
+          file_system::read_dir(webview, path, options, callback, error);
         }
-        #[cfg(any(feature = "all-api", feature = "listFiles"))]
-        ListFiles {
-          path,
-          callback,
-          error,
-        } => {
-          crate::file_system::list(webview, path, callback, error);
-        }
-        #[cfg(any(feature = "all-api", feature = "setTitle"))]
+        #[cfg(any(feature = "all-api", feature = "set-title"))]
         SetTitle { title } => {
           webview.set_title(&title)?;
         }
@@ -83,7 +79,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
           callback,
           error,
         } => {
-          crate::salt::validate(webview, salt, callback, error);
+          salt::validate(webview, salt, callback, error);
         }
         #[cfg(any(feature = "all-api", feature = "event"))]
         Listen {
@@ -108,7 +104,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
           load_asset(webview, asset, asset_type, callback, error)?;
         }
       }
-      Ok(true)
+      Ok(())
     }
   }
 }

+ 14 - 13
tauri/src/endpoints/cmd.rs

@@ -1,41 +1,42 @@
 use serde::Deserialize;
 
+#[derive(Deserialize)]
+pub struct ReadDirOptions {
+  #[serde(default)]
+  pub recursive: bool
+}
+
 #[derive(Deserialize)]
 #[serde(tag = "cmd", rename_all = "camelCase")]
 pub enum Cmd {
   Init {},
-  #[cfg(any(feature = "all-api", feature = "readTextFile"))]
+  #[cfg(any(feature = "all-api", feature = "read-text-file"))]
   ReadTextFile {
     path: String,
     callback: String,
     error: String,
   },
-  #[cfg(any(feature = "all-api", feature = "readBinaryFile"))]
+  #[cfg(any(feature = "all-api", feature = "read-binary-file"))]
   ReadBinaryFile {
     path: String,
     callback: String,
     error: String,
   },
-  #[cfg(any(feature = "all-api", feature = "writeFile"))]
+  #[cfg(any(feature = "all-api", feature = "write-file"))]
   WriteFile {
     file: String,
     contents: String,
     callback: String,
     error: String,
   },
-  #[cfg(any(feature = "all-api", feature = "listFiles"))]
-  ListFiles {
-    path: String,
-    callback: String,
-    error: String,
-  },
-  #[cfg(any(feature = "all-api", feature = "listDirs"))]
-  ListDirs {
+  #[cfg(any(feature = "all-api", feature = "read-dir"))]
+  ReadDir {
     path: String,
+    options: Option<ReadDirOptions>,
     callback: String,
     error: String,
   },
-  #[cfg(any(feature = "all-api", feature = "setTitle"))]
+  #[cfg(any(feature = "all-api", feature = "set-title"))]
   SetTitle {
     title: String,
   },
@@ -64,7 +65,7 @@ pub enum Cmd {
   #[cfg(any(feature = "all-api", feature = "event"))]
   Emit {
     event: String,
-    payload: String,
+    payload: Option<String>,
   },
   #[cfg(not(any(feature = "dev-server", feature = "embedded-server")))]
   LoadAsset {

+ 17 - 21
tauri/src/file_system.rs → tauri/src/endpoints/file_system.rs

@@ -6,36 +6,32 @@ use tauri_api::file;
 use std::fs::File;
 use std::io::Write;
 
-pub fn list<T: 'static>(
-  webview: &mut WebView<'_, T>,
-  path: String,
-  callback: String,
-  error: String,
-) {
-  crate::execute_promise(
-    webview,
-    move || {
-      dir::walk_dir(path)
-        .map_err(|e| crate::ErrorKind::Command(e.to_string()).into())
-        .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
-    },
-    callback,
-    error,
-  );
-}
+use super::cmd::{ReadDirOptions};
 
-pub fn list_dirs<T: 'static>(
+pub fn read_dir<T: 'static>(
   webview: &mut WebView<'_, T>,
   path: String,
+  options: Option<ReadDirOptions>,
   callback: String,
   error: String,
 ) {
   crate::execute_promise(
     webview,
     move || {
-      dir::list_dir_contents(path)
-        .map_err(|e| crate::ErrorKind::Command(e.to_string()).into())
-        .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
+      let recursive = if let Some(options_value) = options {
+        options_value.recursive
+      } else {
+        false
+      };
+      if recursive {
+        dir::walk_dir(path)
+          .map_err(|e| crate::ErrorKind::Command(e.to_string()).into())
+          .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
+      } else {
+        dir::list_dir_contents(path)
+          .map_err(|e| crate::ErrorKind::Command(e.to_string()).into())
+          .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
+      }
     },
     callback,
     error,

+ 18 - 0
tauri/src/endpoints/salt.rs

@@ -0,0 +1,18 @@
+use web_view::WebView;
+
+pub fn validate<T: 'static>(
+  webview: &mut WebView<'_, T>,
+  salt: String,
+  callback: String,
+  error: String,
+) {
+  let response = if crate::salt::is_valid(salt) {
+    Ok("'VALID'".to_string())
+  } else {
+    Err("'INVALID SALT'".to_string())
+  };
+  let callback_string = crate::api::rpc::format_callback_result(response, callback, error);
+  webview
+    .eval(callback_string.as_str())
+    .expect("Failed to eval JS from validate()");
+}

+ 14 - 11
tauri/src/event.rs

@@ -6,7 +6,7 @@ use lazy_static::lazy_static;
 use web_view::Handle;
 
 struct EventHandler {
-  on_event: Box<dyn FnMut(String)>,
+  on_event: Box<dyn FnMut(Option<String>)>,
 }
 
 thread_local!(static LISTENERS: Arc<Mutex<HashMap<String, EventHandler>>> = Arc::new(Mutex::new(HashMap::new())));
@@ -29,7 +29,7 @@ pub fn event_queue_object_name() -> String {
   EVENT_QUEUE_OBJECT_NAME.to_string()
 }
 
-pub fn listen<F: FnMut(String) + 'static>(id: String, handler: F) {
+pub fn listen<F: FnMut(Option<String>) + 'static>(id: String, handler: F) {
   LISTENERS.with(|listeners| {
     let mut l = listeners
       .lock()
@@ -43,11 +43,14 @@ pub fn listen<F: FnMut(String) + 'static>(id: String, handler: F) {
   });
 }
 
-pub fn emit<T: 'static>(webview_handle: &Handle<T>, event: String, mut payload: String) {
+pub fn emit<T: 'static>(webview_handle: &Handle<T>, event: String, payload: Option<String>) {
   let salt = crate::salt::generate();
-  if payload == "" {
-    payload = "void 0".to_string();
-  }
+
+  let js_payload = if let Some(payload_str) = payload {
+    payload_str
+  } else {
+    "void 0".to_string()
+  };
 
   webview_handle
     .dispatch(move |_webview| {
@@ -55,14 +58,14 @@ pub fn emit<T: 'static>(webview_handle: &Handle<T>, event: String, mut payload:
         "window['{}']({{type: '{}', payload: {}}}, '{}')",
         emit_function_name(),
         event.as_str(),
-        payload,
+        js_payload,
         salt
       ))
     })
     .expect("Failed to dispatch JS from emit");
 }
 
-pub fn on_event(event: String, data: String) {
+pub fn on_event(event: String, data: Option<String>) {
   LISTENERS.with(|listeners| {
     let mut l = listeners
       .lock()
@@ -83,8 +86,8 @@ mod test {
   use proptest::prelude::*;
 
   // dummy event handler function
-  fn event_fn(s: String) {
-    println!("{}", s)
+  fn event_fn(s: Option<String>) {
+    println!("{:?}", s);
   }
 
   proptest! {
@@ -143,7 +146,7 @@ mod test {
       // call listen with e and the event_fn dummy func
       listen(e.clone(), event_fn);
       // call on event with e and d.
-      on_event(e, d);
+      on_event(e, Some(d));
 
       // open listeners
       LISTENERS.with(|list| {

+ 0 - 5
tauri/src/lib.rs

@@ -13,12 +13,7 @@ pub mod server;
 mod app;
 mod endpoints;
 #[allow(dead_code)]
-mod file_system;
-#[allow(dead_code)]
 mod salt;
-#[cfg(feature = "embedded-server")]
-mod tcp;
-mod view;
 
 use std::process::Stdio;
 

+ 0 - 18
tauri/src/salt.rs

@@ -2,7 +2,6 @@ use std::sync::Mutex;
 
 use lazy_static::lazy_static;
 use uuid::Uuid;
-use web_view::WebView;
 
 struct Salt {
   value: String,
@@ -49,20 +48,3 @@ pub fn is_valid(salt: String) -> bool {
     None => false,
   }
 }
-
-pub fn validate<T: 'static>(
-  webview: &mut WebView<'_, T>,
-  salt: String,
-  callback: String,
-  error: String,
-) {
-  let response = if is_valid(salt) {
-    Ok("'VALID'".to_string())
-  } else {
-    Err("'INVALID SALT'".to_string())
-  };
-  let callback_string = crate::api::rpc::format_callback_result(response, callback, error);
-  webview
-    .eval(callback_string.as_str())
-    .expect("Failed to eval JS from validate()");
-}

+ 0 - 1
tauri/src/view.rs

@@ -1 +0,0 @@
-