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

This commit is contained in:
Lucas Fernandes Nogueira 2023-05-23 07:39:42 -07:00 committed by GitHub
parent 9770032860
commit 7e5905ae1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 315 additions and 102 deletions

6
.changes/add-command.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)?,
}

View File

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