Browse Source

feat(cli/info): include plugins info (#10729)

* feat(cli/info): include plugins info

closes #10682

* header

* resolve package manager once

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Amr Bashir 11 months ago
parent
commit
91e9e784aa

+ 6 - 0
.changes/cli-info-plugins.md

@@ -0,0 +1,6 @@
+---
+"tauri-cli": "patch:feat"
+"@tauri-apps/cli": "patch:feat"
+---
+
+Add plugins information in `tauri info` output

+ 1 - 1
tooling/cli/node/src/lib.rs

@@ -19,7 +19,7 @@ pub fn run(args: Vec<String>, bin_name: Option<String>, callback: JsFunction) ->
       tauri_cli::try_run(args, bin_name)
     })) {
       Ok(t) => t,
-      Err(e) => {
+      Err(_) => {
         return function.call(
           Err(Error::new(
             Status::GenericFailure,

+ 105 - 97
tooling/cli/src/helpers/cargo_manifest.rs

@@ -6,8 +6,7 @@ use serde::Deserialize;
 
 use std::{
   collections::HashMap,
-  fmt::Write,
-  fs::read_to_string,
+  fs,
   path::{Path, PathBuf},
 };
 
@@ -50,9 +49,63 @@ pub struct CargoManifest {
   pub dependencies: HashMap<String, CargoManifestDependency>,
 }
 
+#[derive(Default)]
 pub struct CrateVersion {
-  pub version: String,
-  pub found_crate_versions: Vec<String>,
+  pub version: Option<String>,
+  pub git: Option<String>,
+  pub git_branch: Option<String>,
+  pub git_rev: Option<String>,
+  pub path: Option<PathBuf>,
+  pub lock_version: Option<String>,
+}
+
+impl CrateVersion {
+  pub fn has_version(&self) -> bool {
+    self.version.is_some() || self.git.is_some() || self.path.is_some()
+  }
+}
+
+impl std::fmt::Display for CrateVersion {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    if let Some(g) = &self.git {
+      if let Some(version) = &self.version {
+        write!(f, "{g} ({version})")?;
+      } else {
+        write!(f, "git:{g}")?;
+        if let Some(branch) = &self.git_branch {
+          write!(f, "&branch={branch}")?;
+        } else if let Some(rev) = &self.git_rev {
+          write!(f, "#rev={rev}")?;
+        }
+      }
+    } else if let Some(p) = &self.path {
+      write!(f, "path:{}", p.display())?;
+      if let Some(version) = &self.version {
+        write!(f, " ({version})")?;
+      }
+    } else if let Some(version) = &self.version {
+      write!(f, "{version}")?;
+    } else {
+      return write!(f, "No version detected");
+    }
+
+    if let Some(lock_version) = &self.lock_version {
+      write!(f, " ({lock_version})")?;
+    }
+
+    Ok(())
+  }
+}
+
+pub fn crate_latest_version(name: &str) -> Option<String> {
+  let url = format!("https://docs.rs/crate/{name}/");
+  match ureq::get(&url).call() {
+    Ok(response) => match (response.status(), response.header("location")) {
+      (302, Some(location)) => Some(location.replace(&url, "")),
+      _ => None,
+    },
+    Err(_) => None,
+  }
 }
 
 pub fn crate_version(
@@ -61,6 +114,8 @@ pub fn crate_version(
   lock: Option<&CargoLock>,
   name: &str,
 ) -> CrateVersion {
+  let mut version = CrateVersion::default();
+
   let crate_lock_packages: Vec<CargoLockPackage> = lock
     .as_ref()
     .map(|lock| {
@@ -72,101 +127,54 @@ pub fn crate_version(
         .collect()
     })
     .unwrap_or_default();
-  let (crate_version_string, found_crate_versions) =
-    match (&manifest, &lock, crate_lock_packages.len()) {
-      (Some(_manifest), Some(_lock), 1) => {
-        let crate_lock_package = crate_lock_packages.first().unwrap();
-        let version_string = if let Some(s) = &crate_lock_package.source {
-          if s.starts_with("git") {
-            format!("{} ({})", s, crate_lock_package.version)
-          } else {
-            crate_lock_package.version.clone()
-          }
-        } else {
-          crate_lock_package.version.clone()
-        };
-        (version_string, vec![crate_lock_package.version.clone()])
-      }
-      (None, Some(_lock), 1) => {
-        let crate_lock_package = crate_lock_packages.first().unwrap();
-        let version_string = if let Some(s) = &crate_lock_package.source {
-          if s.starts_with("git") {
-            format!("{} ({})", s, crate_lock_package.version)
-          } else {
-            crate_lock_package.version.clone()
+
+  if crate_lock_packages.len() == 1 {
+    let crate_lock_package = crate_lock_packages.first().unwrap();
+    if let Some(s) = crate_lock_package
+      .source
+      .as_ref()
+      .filter(|s| s.starts_with("git"))
+    {
+      version.git = Some(s.clone());
+    }
+
+    version.version = Some(crate_lock_package.version.clone());
+  } else {
+    if let Some(dep) = manifest.and_then(|m| m.dependencies.get(name).cloned()) {
+      match dep {
+        CargoManifestDependency::Version(v) => version.version = Some(v),
+        CargoManifestDependency::Package(p) => {
+          if let Some(v) = p.version {
+            version.version = Some(v);
+          } else if let Some(p) = p.path {
+            let manifest_path = tauri_dir.join(&p).join("Cargo.toml");
+            let v = fs::read_to_string(manifest_path)
+              .ok()
+              .and_then(|m| toml::from_str::<CargoManifest>(&m).ok())
+              .map(|m| m.package.version);
+            version.version = v;
+            version.path = Some(p);
+          } else if let Some(g) = p.git {
+            version.git = Some(g);
+            version.git_branch = p.branch;
+            version.git_rev = p.rev;
           }
-        } else {
-          crate_lock_package.version.clone()
-        };
-        (
-          format!("{version_string} (no manifest)"),
-          vec![crate_lock_package.version.clone()],
-        )
-      }
-      _ => {
-        let mut found_crate_versions = Vec::new();
-        let mut is_git = false;
-        let manifest_version = match manifest.and_then(|m| m.dependencies.get(name).cloned()) {
-          Some(tauri) => match tauri {
-            CargoManifestDependency::Version(v) => {
-              found_crate_versions.push(v.clone());
-              v
-            }
-            CargoManifestDependency::Package(p) => {
-              if let Some(v) = p.version {
-                found_crate_versions.push(v.clone());
-                v
-              } else if let Some(p) = p.path {
-                let manifest_path = tauri_dir.join(&p).join("Cargo.toml");
-                let v = match read_to_string(manifest_path)
-                  .map_err(|_| ())
-                  .and_then(|m| toml::from_str::<CargoManifest>(&m).map_err(|_| ()))
-                {
-                  Ok(manifest) => manifest.package.version,
-                  Err(_) => "unknown version".to_string(),
-                };
-                format!("path:{p:?} [{v}]")
-              } else if let Some(g) = p.git {
-                is_git = true;
-                let mut v = format!("git:{g}");
-                if let Some(branch) = p.branch {
-                  let _ = write!(v, "&branch={branch}");
-                } else if let Some(rev) = p.rev {
-                  let _ = write!(v, "#{rev}");
-                }
-                v
-              } else {
-                "unknown manifest".to_string()
-              }
-            }
-          },
-          None => "no manifest".to_string(),
-        };
-
-        let lock_version = match (lock, crate_lock_packages.is_empty()) {
-          (Some(_lock), false) => crate_lock_packages
-            .iter()
-            .map(|p| p.version.clone())
-            .collect::<Vec<String>>()
-            .join(", "),
-          (Some(_lock), true) => "unknown lockfile".to_string(),
-          _ => "no lockfile".to_string(),
-        };
-
-        (
-          format!(
-            "{} {}({})",
-            manifest_version,
-            if is_git { "(git manifest)" } else { "" },
-            lock_version
-          ),
-          found_crate_versions,
-        )
+        }
       }
-    };
+    }
+
+    if lock.is_some() && crate_lock_packages.is_empty() {
+      let lock_version = crate_lock_packages
+        .iter()
+        .map(|p| p.version.clone())
+        .collect::<Vec<String>>()
+        .join(", ");
 
-  CrateVersion {
-    found_crate_versions,
-    version: crate_version_string,
+      if !lock_version.is_empty() {
+        version.lock_version = Some(lock_version);
+      }
+    }
   }
+
+  version
 }

+ 18 - 3
tooling/cli/src/info/mod.rs

@@ -17,6 +17,7 @@ mod env_system;
 mod ios;
 mod packages_nodejs;
 mod packages_rust;
+mod plugins;
 
 #[derive(Deserialize)]
 struct JsCliVersionMetadata {
@@ -265,6 +266,11 @@ pub fn command(options: Options) -> Result<()> {
     crate::helpers::app_paths::resolve();
   }
 
+  let package_manager = app_dir
+    .as_ref()
+    .map(packages_nodejs::package_manager)
+    .unwrap_or(crate::helpers::npm::PackageManager::Npm);
+
   let metadata = version_metadata()?;
 
   let mut environment = Section {
@@ -285,9 +291,17 @@ pub fn command(options: Options) -> Result<()> {
   packages
     .items
     .extend(packages_rust::items(app_dir.as_ref(), tauri_dir.as_deref()));
-  packages
-    .items
-    .extend(packages_nodejs::items(app_dir.as_ref(), &metadata));
+  packages.items.extend(packages_nodejs::items(
+    app_dir.as_ref(),
+    package_manager,
+    &metadata,
+  ));
+
+  let mut plugins = Section {
+    label: "Plugins",
+    interactive,
+    items: plugins::items(app_dir.as_ref(), tauri_dir.as_deref(), package_manager),
+  };
 
   let mut app = Section {
     label: "App",
@@ -300,6 +314,7 @@ pub fn command(options: Options) -> Result<()> {
 
   environment.display();
   packages.display();
+  plugins.display();
   app.display();
 
   // iOS

+ 72 - 70
tooling/cli/src/info/packages_nodejs.rs

@@ -15,7 +15,7 @@ struct YarnVersionInfo {
   data: Vec<String>,
 }
 
-fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Option<String>> {
+pub fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Option<String>> {
   match pm {
     PackageManager::Yarn => {
       let mut cmd = cross_command("yarn");
@@ -87,21 +87,22 @@ fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Option<S
   }
 }
 
-fn get_package_manager<T: AsRef<str>>(app_dir_entries: &[T]) -> PackageManager {
+pub fn package_manager(app_dir: &PathBuf) -> PackageManager {
   let mut use_npm = false;
   let mut use_pnpm = false;
   let mut use_yarn = false;
   let mut use_bun = 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;
-    } else if name.as_ref() == "bun.lockb" {
-      use_bun = true;
+  for entry in std::fs::read_dir(app_dir)
+    .unwrap()
+    .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
+  {
+    match entry.as_str() {
+      "pnpm-lock.yaml" => use_pnpm = true,
+      "package-lock.json" => use_npm = true,
+      "yarn.lock" => use_yarn = true,
+      "bun.lockb" => use_bun = true,
+      _ => {}
     }
   }
 
@@ -131,11 +132,11 @@ fn get_package_manager<T: AsRef<str>>(app_dir_entries: &[T]) -> PackageManager {
   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
-    );
+          "{}: 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;
   }
 
@@ -145,29 +146,21 @@ fn get_package_manager<T: AsRef<str>>(app_dir_entries: &[T]) -> PackageManager {
     PackageManager::Pnpm
   } else if use_bun {
     PackageManager::Bun
+  } else if manager_version("yarn")
+    .map(|v| v.chars().next().map(|c| c > '1').unwrap_or_default())
+    .unwrap_or(false)
+  {
+    PackageManager::YarnBerry
   } else {
     PackageManager::Yarn
   }
 }
 
-pub fn items(app_dir: Option<&PathBuf>, metadata: &VersionMetadata) -> 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);
-  }
-
-  if package_manager == PackageManager::Yarn
-    && manager_version("yarn")
-      .map(|v| v.chars().next().map(|c| c > '1').unwrap_or_default())
-      .unwrap_or(false)
-  {
-    package_manager = PackageManager::YarnBerry;
-  }
-
+pub fn items(
+  app_dir: Option<&PathBuf>,
+  package_manager: PackageManager,
+  metadata: &VersionMetadata,
+) -> Vec<SectionItem> {
   let mut items = Vec::new();
   if let Some(app_dir) = app_dir {
     for (package, version) in [
@@ -175,45 +168,54 @@ pub fn items(app_dir: Option<&PathBuf>, metadata: &VersionMetadata) -> Vec<Secti
       ("@tauri-apps/cli", Some(metadata.js_cli.version.clone())),
     ] {
       let app_dir = app_dir.clone();
-      let item = SectionItem::new().action(move || {
-        let version = version.clone().unwrap_or_else(|| {
-          package_manager
-            .current_package_version(package, &app_dir)
-            .unwrap_or_default()
-            .unwrap_or_default()
-        });
-        let latest_ver = npm_latest_version(&package_manager, package)
-          .unwrap_or_default()
-          .unwrap_or_default();
-
-        if version.is_empty() {
-          format!("{} {}: not installed!", package, "".green())
-        } else {
-          format!(
-            "{} {}: {}{}",
-            package,
-            "[NPM]".dimmed(),
-            version,
-            if !(version.is_empty() || latest_ver.is_empty()) {
-              let version = semver::Version::parse(version.as_str()).unwrap();
-              let target_version = semver::Version::parse(latest_ver.as_str()).unwrap();
-
-              if version < target_version {
-                format!(" ({}, latest: {})", "outdated".yellow(), latest_ver.green())
-              } else {
-                "".into()
-              }
-            } else {
-              "".into()
-            }
-          )
-        }
-        .into()
-      });
-
+      let item = nodejs_section_item(package.into(), version, app_dir, package_manager);
       items.push(item);
     }
   }
 
   items
 }
+
+pub fn nodejs_section_item(
+  package: String,
+  version: Option<String>,
+  app_dir: PathBuf,
+  package_manager: PackageManager,
+) -> SectionItem {
+  SectionItem::new().action(move || {
+    let version = version.clone().unwrap_or_else(|| {
+      package_manager
+        .current_package_version(&package, &app_dir)
+        .unwrap_or_default()
+        .unwrap_or_default()
+    });
+
+    let latest_ver = super::packages_nodejs::npm_latest_version(&package_manager, &package)
+      .unwrap_or_default()
+      .unwrap_or_default();
+
+    if version.is_empty() {
+      format!("{} {}: not installed!", package, "".green())
+    } else {
+      format!(
+        "{} {}: {}{}",
+        package,
+        "".dimmed(),
+        version,
+        if !(version.is_empty() || latest_ver.is_empty()) {
+          let version = semver::Version::parse(version.as_str()).unwrap();
+          let target_version = semver::Version::parse(latest_ver.as_str()).unwrap();
+
+          if version < target_version {
+            format!(" ({}, latest: {})", "outdated".yellow(), latest_ver.green())
+          } else {
+            "".into()
+          }
+        } else {
+          "".into()
+        }
+      )
+    }
+    .into()
+  })
+}

+ 40 - 46
tooling/cli/src/info/packages_rust.rs

@@ -4,24 +4,15 @@
 
 use super::{ActionResult, SectionItem};
 use crate::{
-  helpers::cargo_manifest::{crate_version, CargoLock, CargoManifest},
+  helpers::cargo_manifest::{
+    crate_latest_version, crate_version, CargoLock, CargoManifest, CrateVersion,
+  },
   interface::rust::get_workspace_dir,
 };
 use colored::Colorize;
 use std::fs::read_to_string;
 use std::path::{Path, PathBuf};
 
-fn crate_latest_version(name: &str) -> Option<String> {
-  let url = format!("https://docs.rs/crate/{name}/");
-  match ureq::get(&url).call() {
-    Ok(response) => match (response.status(), response.header("location")) {
-      (302, Some(location)) => Some(location.replace(&url, "")),
-      _ => None,
-    },
-    Err(_) => None,
-  }
-}
-
 pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<SectionItem> {
   let mut items = Vec::new();
 
@@ -39,39 +30,8 @@ pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<Section
         .and_then(|s| toml::from_str(&s).ok());
 
       for dep in ["tauri", "tauri-build", "wry", "tao"] {
-        let version = crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
-        let crate_version = version
-          .found_crate_versions
-          .into_iter()
-          .map(|v| semver::Version::parse(&v).ok())
-          .max();
-
-        let version_suffix = match (crate_version, crate_latest_version(dep)) {
-          (Some(Some(version)), Some(target_version)) => {
-            let target_version = semver::Version::parse(&target_version).unwrap();
-            if version < target_version {
-              Some(format!(
-                " ({}, latest: {})",
-                "outdated".yellow(),
-                target_version.to_string().green()
-              ))
-            } else {
-              None
-            }
-          }
-          _ => None,
-        };
-
-        let item = SectionItem::new().description(format!(
-          "{} {}: {}{}",
-          dep,
-          "[RUST]".dimmed(),
-          version.version,
-          version_suffix
-            .clone()
-            .map(|s| format!(",{s}"))
-            .unwrap_or_else(|| "".into())
-        ));
+        let crate_version = crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
+        let item = rust_section_item(dep, crate_version);
         items.push(item);
       }
     }
@@ -91,7 +51,7 @@ pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<Section
           format!(
             "{} {}: {}{}",
             package,
-            "[RUST]".dimmed(),
+            "🦀",
             version.split_once('\n').unwrap_or_default().0,
             if !(version.is_empty() || latest_ver.is_empty()) {
               let version = semver::Version::parse(version).unwrap();
@@ -117,3 +77,37 @@ pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<Section
 
   items
 }
+
+pub fn rust_section_item(dep: &str, crate_version: CrateVersion) -> SectionItem {
+  let version = crate_version
+    .version
+    .as_ref()
+    .and_then(|v| semver::Version::parse(v).ok());
+
+  let version_suffix = match (version, crate_latest_version(dep)) {
+    (Some(version), Some(target_version)) => {
+      let target_version = semver::Version::parse(&target_version).unwrap();
+      if version < target_version {
+        Some(format!(
+          " ({}, latest: {})",
+          "outdated".yellow(),
+          target_version.to_string().green()
+        ))
+      } else {
+        None
+      }
+    }
+    _ => None,
+  };
+
+  SectionItem::new().description(format!(
+    "{} {}: {}{}",
+    dep,
+    "🦀",
+    crate_version,
+    version_suffix
+      .clone()
+      .map(|s| format!(",{s}"))
+      .unwrap_or_else(|| "".into())
+  ))
+}

+ 65 - 0
tooling/cli/src/info/plugins.rs

@@ -0,0 +1,65 @@
+// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{
+  fs,
+  path::{Path, PathBuf},
+};
+
+use crate::{
+  helpers::{
+    self,
+    cargo_manifest::{crate_version, CargoLock, CargoManifest},
+    npm::PackageManager,
+  },
+  interface::rust::get_workspace_dir,
+};
+
+use super::{packages_nodejs, packages_rust, SectionItem};
+
+pub fn items(
+  app_dir: Option<&PathBuf>,
+  tauri_dir: Option<&Path>,
+  package_manager: PackageManager,
+) -> Vec<SectionItem> {
+  let mut items = Vec::new();
+
+  if tauri_dir.is_some() || app_dir.is_some() {
+    if let Some(tauri_dir) = tauri_dir {
+      let manifest: Option<CargoManifest> =
+        if let Ok(manifest_contents) = fs::read_to_string(tauri_dir.join("Cargo.toml")) {
+          toml::from_str(&manifest_contents).ok()
+        } else {
+          None
+        };
+
+      let lock: Option<CargoLock> = get_workspace_dir()
+        .ok()
+        .and_then(|p| fs::read_to_string(p.join("Cargo.lock")).ok())
+        .and_then(|s| toml::from_str(&s).ok());
+
+      for p in helpers::plugins::known_plugins().keys() {
+        let dep = format!("tauri-plugin-{p}");
+        let crate_version = crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), &dep);
+        if !crate_version.has_version() {
+          continue;
+        }
+        let item = packages_rust::rust_section_item(&dep, crate_version);
+        items.push(item);
+
+        let Some(app_dir) = app_dir else {
+          continue;
+        };
+
+        let package = format!("@tauri-apps/plugin-{p}");
+
+        let item =
+          packages_nodejs::nodejs_section_item(package, None, app_dir.clone(), package_manager);
+        items.push(item);
+      }
+    }
+  }
+
+  items
+}

+ 3 - 1
tooling/cli/src/migrate/mod.rs

@@ -38,7 +38,9 @@ pub fn command() -> Result<()> {
     None
   };
 
-  let tauri_version = crate_version(tauri_dir, Some(&manifest), lock.as_ref(), "tauri").version;
+  let tauri_version = crate_version(tauri_dir, Some(&manifest), lock.as_ref(), "tauri")
+    .version
+    .context("failed to get tauri version")?;
   let tauri_version = semver::Version::from_str(&tauri_version)?;
 
   if tauri_version.major == 1 {