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>
This commit is contained in:
Amr Bashir
2024-08-23 14:29:03 +03:00
committed by GitHub
parent 07aff5a2d4
commit 91e9e784aa
8 changed files with 310 additions and 218 deletions

View File

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

View File

@@ -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,

View File

@@ -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()
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()
};
(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()
}
} else {
crate_lock_package.version.clone()
};
(
format!("{version_string} (no manifest)"),
vec![crate_lock_package.version.clone()],
)
}
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(", ");
if !lock_version.is_empty() {
version.lock_version = Some(lock_version);
}
_ => {
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,
)
}
};
CrateVersion {
found_crate_versions,
version: crate_version_string,
}
}
version
}

View File

@@ -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

View File

@@ -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()
})
}

View File

@@ -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())
))
}

View File

@@ -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
}

View File

@@ -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 {