فهرست منبع

feat(cli): `plugin add` command (#7023)

Lucas Fernandes Nogueira 2 سال پیش
والد
کامیت
7e5905ae1d

+ 6 - 0
.changes/add-command.md

@@ -0,0 +1,6 @@
+---
+"cli.rs": patch
+"cli.js": patch
+---
+
+Added `tauri plugin add` command to add a plugin to the Tauri project.

+ 14 - 0
tooling/cli/src/helpers/mod.rs

@@ -6,6 +6,7 @@ pub mod app_paths;
 pub mod config;
 pub mod flock;
 pub mod framework;
+pub mod npm;
 pub mod template;
 pub mod updater_signature;
 pub mod web_dev_server;
@@ -13,6 +14,7 @@ pub mod web_dev_server;
 use std::{
   collections::HashMap,
   path::{Path, PathBuf},
+  process::Command,
 };
 
 pub fn command_env(debug: bool) -> HashMap<&'static str, String> {
@@ -38,3 +40,15 @@ pub fn resolve_tauri_path<P: AsRef<Path>>(path: P, crate_name: &str) -> PathBuf
     PathBuf::from("..").join(path).join(crate_name)
   }
 }
+
+pub fn cross_command(bin: &str) -> Command {
+  #[cfg(target_os = "windows")]
+  let cmd = {
+    let mut cmd = Command::new("cmd");
+    cmd.arg("/c").arg(bin);
+    cmd
+  };
+  #[cfg(not(target_os = "windows"))]
+  let cmd = Command::new(bin);
+  cmd
+}

+ 68 - 0
tooling/cli/src/helpers/npm.rs

@@ -0,0 +1,68 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{fmt::Display, path::Path};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum PackageManager {
+  Npm,
+  Pnpm,
+  Yarn,
+  YarnBerry,
+}
+
+impl Display for PackageManager {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    write!(
+      f,
+      "{}",
+      match self {
+        PackageManager::Npm => "npm",
+        PackageManager::Pnpm => "pnpm",
+        PackageManager::Yarn => "yarn",
+        PackageManager::YarnBerry => "yarn berry",
+      }
+    )
+  }
+}
+
+impl PackageManager {
+  pub fn from_project<P: AsRef<Path>>(path: P) -> Vec<Self> {
+    let mut use_npm = false;
+    let mut use_pnpm = false;
+    let mut use_yarn = false;
+
+    if let Ok(entries) = std::fs::read_dir(path) {
+      for entry in entries.flatten() {
+        let path = entry.path();
+        let name = path.file_name().unwrap().to_string_lossy();
+        if name.as_ref() == "package-lock.json" {
+          use_npm = true;
+        } else if name.as_ref() == "pnpm-lock.yaml" {
+          use_pnpm = true;
+        } else if name.as_ref() == "yarn.lock" {
+          use_yarn = true;
+        }
+      }
+    }
+
+    if !use_npm && !use_pnpm && !use_yarn {
+      return Vec::new();
+    }
+
+    let mut found = Vec::new();
+
+    if use_npm {
+      found.push(PackageManager::Npm);
+    }
+    if use_pnpm {
+      found.push(PackageManager::Pnpm);
+    }
+    if use_yarn {
+      found.push(PackageManager::Yarn);
+    }
+
+    found
+  }
+}

+ 3 - 1
tooling/cli/src/info/env_nodejs.rs

@@ -2,10 +2,12 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{cross_command, VersionMetadata};
+use super::VersionMetadata;
 use super::{SectionItem, Status};
 use colored::Colorize;
 
+use crate::helpers::cross_command;
+
 pub fn items(metadata: &VersionMetadata) -> (Vec<SectionItem>, Option<String>) {
   let yarn_version = cross_command("yarn")
     .arg("-v")

+ 0 - 13
tooling/cli/src/info/mod.rs

@@ -10,7 +10,6 @@ use serde::Deserialize;
 use std::{
   fmt::{self, Display, Formatter},
   panic,
-  process::Command,
 };
 
 mod app;
@@ -73,18 +72,6 @@ pub(crate) fn cli_upstream_version() -> Result<String> {
     .map_err(|e| anyhow::Error::new(e))
 }
 
-pub fn cross_command(bin: &str) -> Command {
-  #[cfg(target_os = "windows")]
-  let cmd = {
-    let mut cmd = Command::new("cmd");
-    cmd.arg("/c").arg(bin);
-    cmd
-  };
-  #[cfg(not(target_os = "windows"))]
-  let cmd = Command::new(bin);
-  cmd
-}
-
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
 pub enum Status {
   Neutral = 0,

+ 28 - 88
tooling/cli/src/info/packages_nodejs.rs

@@ -2,41 +2,19 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use super::{cross_command, VersionMetadata};
+use super::VersionMetadata;
 use super::{SectionItem, Status};
 use colored::Colorize;
 use serde::Deserialize;
-use std::fmt::Display;
 use std::path::{Path, PathBuf};
 
+use crate::helpers::{cross_command, npm::PackageManager};
+
 #[derive(Deserialize)]
 struct YarnVersionInfo {
   data: Vec<String>,
 }
 
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-enum PackageManager {
-  Npm,
-  Pnpm,
-  Yarn,
-  YarnBerry,
-}
-
-impl Display for PackageManager {
-  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-    write!(
-      f,
-      "{}",
-      match self {
-        PackageManager::Npm => "npm",
-        PackageManager::Pnpm => "pnpm",
-        PackageManager::Yarn => "yarn",
-        PackageManager::YarnBerry => "yarn berry",
-      }
-    )
-  }
-}
-
 fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Option<String>> {
   match pm {
     PackageManager::Yarn => {
@@ -154,74 +132,36 @@ fn npm_package_version<P: AsRef<Path>>(
   }
 }
 
-fn get_package_manager<T: AsRef<str>>(app_dir_entries: &[T]) -> PackageManager {
-  let mut use_npm = false;
-  let mut use_pnpm = false;
-  let mut use_yarn = false;
-
-  for name in app_dir_entries {
-    if name.as_ref() == "package-lock.json" {
-      use_npm = true;
-    } else if name.as_ref() == "pnpm-lock.yaml" {
-      use_pnpm = true;
-    } else if name.as_ref() == "yarn.lock" {
-      use_yarn = true;
-    }
-  }
-
-  if !use_npm && !use_pnpm && !use_yarn {
-    println!(
-      "{}: no lock files found, defaulting to npm",
-      "WARNING".yellow()
-    );
-    return PackageManager::Npm;
-  }
-
-  let mut found = Vec::new();
-
-  if use_npm {
-    found.push(PackageManager::Npm);
-  }
-  if use_pnpm {
-    found.push(PackageManager::Pnpm);
-  }
-  if use_yarn {
-    found.push(PackageManager::Yarn);
-  }
-
-  if found.len() > 1 {
-    let pkg_manger = found[0];
-    println!(
-      "{}: Only one package manager should be used, but found {}.\n         Please remove unused package manager lock files, will use {} for now!",
-      "WARNING".yellow(),
-      found.iter().map(ToString::to_string).collect::<Vec<_>>().join(" and "),
-      pkg_manger
-    );
-    return pkg_manger;
-  }
-
-  if use_npm {
-    PackageManager::Npm
-  } else if use_pnpm {
-    PackageManager::Pnpm
-  } else {
-    PackageManager::Yarn
-  }
-}
-
 pub fn items(
   app_dir: Option<&PathBuf>,
   metadata: &VersionMetadata,
   yarn_version: Option<String>,
 ) -> Vec<SectionItem> {
-  let mut package_manager = PackageManager::Npm;
-  if let Some(app_dir) = &app_dir {
-    let app_dir_entries = std::fs::read_dir(app_dir)
-      .unwrap()
-      .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
-      .collect::<Vec<String>>();
-    package_manager = get_package_manager(&app_dir_entries);
-  }
+  let package_managers = app_dir
+    .map(PackageManager::from_project)
+    .unwrap_or_else(|| {
+      println!(
+        "{}: no lock files found, defaulting to npm",
+        "WARNING".yellow()
+      );
+      vec![PackageManager::Npm]
+    });
+
+  let mut package_manager = if package_managers.len() > 1 {
+    let pkg_manager = package_managers[0];
+    println!(
+          "{}: Only one package manager should be used, but found {}.\n         Please remove unused package manager lock files, will use {} for now!",
+          "WARNING".yellow(),
+          package_managers.iter().map(ToString::to_string).collect::<Vec<_>>().join(" and "),
+          pkg_manager
+        );
+    pkg_manager
+  } else {
+    package_managers
+      .into_iter()
+      .next()
+      .unwrap_or(PackageManager::Npm)
+  };
 
   if package_manager == PackageManager::Yarn
     && yarn_version

+ 3 - 0
tooling/cli/src/plugin.rs

@@ -6,6 +6,7 @@ use clap::{Parser, Subcommand};
 
 use crate::Result;
 
+mod add;
 mod android;
 mod init;
 mod ios;
@@ -26,6 +27,7 @@ pub struct Cli {
 #[derive(Subcommand)]
 enum Commands {
   Init(init::Options),
+  Add(add::Options),
   Android(android::Cli),
   Ios(ios::Cli),
 }
@@ -33,6 +35,7 @@ enum Commands {
 pub fn command(cli: Cli) -> Result<()> {
   match cli.command {
     Commands::Init(options) => init::command(options)?,
+    Commands::Add(options) => add::command(options)?,
     Commands::Android(cli) => android::command(cli)?,
     Commands::Ios(cli) => ios::command(cli)?,
   }

+ 193 - 0
tooling/cli/src/plugin/add.rs

@@ -0,0 +1,193 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use anyhow::Context;
+use clap::Parser;
+
+use crate::{
+  helpers::{
+    app_paths::{app_dir, tauri_dir},
+    cross_command,
+    npm::PackageManager,
+  },
+  Result,
+};
+
+use std::{collections::HashMap, process::Command};
+
+#[derive(Debug, Parser)]
+#[clap(about = "Installs a plugin on the project")]
+pub struct Options {
+  /// The plugin to add.
+  plugin: String,
+  /// Git tag to use.
+  #[clap(short, long)]
+  tag: Option<String>,
+  /// Git rev to use.
+  #[clap(short, long)]
+  rev: Option<String>,
+  /// Git branch to use.
+  #[clap(short, long)]
+  branch: Option<String>,
+}
+
+pub fn command(options: Options) -> Result<()> {
+  let plugin = options.plugin;
+  let crate_name = format!("tauri-plugin-{plugin}");
+  let npm_name = format!("@tauri-apps/plugin-{plugin}");
+
+  let mut plugins = plugins();
+  let metadata = plugins.remove(plugin.as_str()).unwrap_or_default();
+
+  let mut cargo = Command::new("cargo");
+  cargo.current_dir(tauri_dir()).arg("add").arg(&crate_name);
+
+  if options.tag.is_some() || options.rev.is_some() || options.branch.is_some() {
+    cargo
+      .arg("--git")
+      .arg("https://github.com/tauri-apps/plugins-workspace");
+  }
+
+  if metadata.desktop_only {
+    cargo
+      .arg("--target")
+      .arg(r#"cfg(not(any(target_os = "android", target_os = "ios")))"#);
+  }
+
+  let npm_spec = match (options.tag, options.rev, options.branch) {
+    (Some(tag), None, None) => {
+      cargo.args(["--tag", &tag]);
+      format!("tauri-apps/tauri-plugin-{plugin}#{tag}")
+    }
+    (None, Some(rev), None) => {
+      cargo.args(["--rev", &rev]);
+      format!("tauri-apps/tauri-plugin-{plugin}#{rev}")
+    }
+    (None, None, Some(branch)) => {
+      cargo.args(["--branch", &branch]);
+      format!("tauri-apps/tauri-plugin-{plugin}#{branch}")
+    }
+    (None, None, None) => npm_name,
+    _ => anyhow::bail!("Only one of --tag, --rev and --branch can be specified"),
+  };
+
+  log::info!("Installing Cargo dependency {crate_name}...");
+  let status = cargo.status().context("failed to run `cargo add`")?;
+  if !status.success() {
+    anyhow::bail!("Failed to install Cargo dependency");
+  }
+
+  if !metadata.rust_only {
+    if let Some(manager) = std::panic::catch_unwind(app_dir)
+      .map(Some)
+      .unwrap_or_default()
+      .map(PackageManager::from_project)
+      .and_then(|managers| managers.into_iter().next())
+    {
+      let mut cmd = match manager {
+        PackageManager::Npm => cross_command("npm"),
+        PackageManager::Pnpm => cross_command("pnpm"),
+        PackageManager::Yarn => cross_command("yarn"),
+        PackageManager::YarnBerry => cross_command("yarn"),
+      };
+
+      cmd.arg("add").arg(&npm_spec);
+
+      log::info!("Installing NPM dependency {npm_spec}...");
+      let status = cmd
+        .status()
+        .with_context(|| format!("failed to run {manager}"))?;
+      if !status.success() {
+        anyhow::bail!("Failed to install NPM dependency");
+      }
+    }
+  }
+
+  let rust_code = if metadata.builder {
+    if metadata.desktop_only {
+      format!(
+        r#"tauri::Builder::default()
+    .setup(|app| {{
+        #[cfg(desktop)]
+        app.handle().plugin(tauri_plugin_{plugin}::Builder::new().build());
+        Ok(())
+    }})
+    "#,
+      )
+    } else {
+      format!(
+        r#"tauri::Builder::default()
+    .setup(|app| {{
+        app.handle().plugin(tauri_plugin_{plugin}::Builder::new().build());
+        Ok(())
+    }})
+    "#,
+      )
+    }
+  } else if metadata.desktop_only {
+    format!(
+      r#"tauri::Builder::default()
+    .setup(|app| {{
+        #[cfg(desktop)]
+        app.handle().plugin(tauri_plugin_{plugin}::init());
+        Ok(())
+    }})
+    "#,
+    )
+  } else {
+    format!(
+      r#"tauri::Builder::default().plugin(tauri_plugin_{plugin}::init())
+    "#,
+    )
+  };
+
+  println!("You must enable the plugin in your Rust code:\n\n{rust_code}");
+
+  Ok(())
+}
+
+#[derive(Default)]
+struct PluginMetadata {
+  desktop_only: bool,
+  rust_only: bool,
+  builder: bool,
+}
+
+// known plugins with particular cases
+fn plugins() -> HashMap<&'static str, PluginMetadata> {
+  let mut plugins: HashMap<&'static str, PluginMetadata> = HashMap::new();
+
+  // desktop-only
+  for p in [
+    "authenticator",
+    "cli",
+    "global-shortcut",
+    "updater",
+    "window-state",
+  ] {
+    plugins.entry(p).or_default().desktop_only = true;
+  }
+
+  // uses builder pattern
+  for p in [
+    "global-shortcut",
+    "localhost",
+    "log",
+    "sql",
+    "store",
+    "stronghold",
+    "updater",
+    "window-state",
+  ] {
+    plugins.entry(p).or_default().builder = true;
+  }
+
+  // rust-only
+  #[allow(clippy::single_element_loop)]
+  for p in ["localhost"] {
+    plugins.entry(p).or_default().rust_only = true;
+  }
+
+  plugins
+}