refactor(cli): enhance plugin subcommand, closes #7749 (#7990)

Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>
This commit is contained in:
Amr Bashir 2023-10-11 18:34:33 +03:00 committed by GitHub
parent 713f84db2b
commit 4caa1cca99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 189 additions and 46 deletions

View File

@ -0,0 +1,12 @@
---
'tauri-cli': 'patch:breaking'
'@tauri-apps/cli': 'patch:breaking'
---
The `tauri plugin` subcommand is receving a couple of consitency and quality of life improvements:
- Renamed `tauri plugin android/ios add` command to `tauri plugin android/ios init` to match the `tauri plugin init` command.
- Removed the `-n/--name` argument from the `tauri plugin init`, `tauri plugin android/ios init`, and is now parsed from the first positional argument.
- Added `tauri plugin new` to create a plugin in a new directory.
- Changed `tauri plugin init` to initalize a plugin in an existing directory (defaults to current directory) instead of creating a new one.
- Changed `tauri plugin init` to NOT generate mobile projects by default, you can opt-in to generate them using `--android` and `--ios` flags or `--mobile` flag or initalize them later using `tauri plugin android/ios init`.

View File

@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use std::path::Path;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use crate::Result; use crate::Result;
@ -9,6 +11,7 @@ use crate::Result;
mod android; mod android;
mod init; mod init;
mod ios; mod ios;
mod new;
#[derive(Parser)] #[derive(Parser)]
#[clap( #[clap(
@ -25,6 +28,7 @@ pub struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
New(new::Options),
Init(init::Options), Init(init::Options),
Android(android::Cli), Android(android::Cli),
Ios(ios::Cli), Ios(ios::Cli),
@ -32,6 +36,7 @@ enum Commands {
pub fn command(cli: Cli) -> Result<()> { pub fn command(cli: Cli) -> Result<()> {
match cli.command { match cli.command {
Commands::New(options) => new::command(options)?,
Commands::Init(options) => init::command(options)?, Commands::Init(options) => init::command(options)?,
Commands::Android(cli) => android::command(cli)?, Commands::Android(cli) => android::command(cli)?,
Commands::Ios(cli) => ios::command(cli)?, Commands::Ios(cli) => ios::command(cli)?,
@ -39,3 +44,30 @@ pub fn command(cli: Cli) -> Result<()> {
Ok(()) Ok(())
} }
fn infer_plugin_name<P: AsRef<Path>>(directory: P) -> Result<String> {
let dir = directory.as_ref();
let cargo_toml_path = dir.join("Cargo.toml");
let name = if cargo_toml_path.exists() {
let contents = std::fs::read(cargo_toml_path)?;
let cargo_toml: toml::Value = toml::from_slice(&contents)?;
cargo_toml
.get("package")
.and_then(|v| v.get("name"))
.map(|v| v.as_str().unwrap_or_default())
.unwrap_or_default()
.to_string()
} else {
dir
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
};
Ok(
name
.strip_prefix("tauri-plugin-")
.unwrap_or(&name)
.to_string(),
)
}

View File

@ -28,15 +28,15 @@ pub struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
Add(AddOptions), Init(InitOptions),
} }
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[clap(about = "Adds the Android project to an existing Tauri plugin")] #[clap(about = "Initializes the Android project for an existing Tauri plugin")]
pub struct AddOptions { pub struct InitOptions {
/// Name of your Tauri plugin. Must match the current plugin's name. /// Name of your Tauri plugin. Must match the current plugin's name.
#[clap(short = 'n', long = "name")] /// If not specified, it will be infered from the current directory.
plugin_name: String, plugin_name: Option<String>,
/// The output directory. /// The output directory.
#[clap(short, long)] #[clap(short, long)]
#[clap(default_value_t = current_dir().expect("failed to read cwd").to_string_lossy().into_owned())] #[clap(default_value_t = current_dir().expect("failed to read cwd").to_string_lossy().into_owned())]
@ -45,7 +45,12 @@ pub struct AddOptions {
pub fn command(cli: Cli) -> Result<()> { pub fn command(cli: Cli) -> Result<()> {
match cli.command { match cli.command {
Commands::Add(options) => { Commands::Init(options) => {
let plugin_name = match options.plugin_name {
None => super::infer_plugin_name(std::env::current_dir()?)?,
Some(name) => name,
};
let out_dir = PathBuf::from(options.out_dir); let out_dir = PathBuf::from(options.out_dir);
if out_dir.join("android").exists() { if out_dir.join("android").exists() {
return Err(anyhow::anyhow!("android folder already exists")); return Err(anyhow::anyhow!("android folder already exists"));
@ -53,7 +58,7 @@ pub fn command(cli: Cli) -> Result<()> {
let plugin_id = super::init::request_input( let plugin_id = super::init::request_input(
"What should be the Android Package ID for your plugin?", "What should be the Android Package ID for your plugin?",
Some(format!("com.plugin.{}", options.plugin_name)), Some(format!("com.plugin.{}", plugin_name)),
false, false,
false, false,
)? )?
@ -62,7 +67,7 @@ pub fn command(cli: Cli) -> Result<()> {
let handlebars = Handlebars::new(); let handlebars = Handlebars::new();
let mut data = BTreeMap::new(); let mut data = BTreeMap::new();
super::init::plugin_name_data(&mut data, &options.plugin_name); super::init::plugin_name_data(&mut data, &plugin_name);
let mut created_dirs = Vec::new(); let mut created_dirs = Vec::new();
template::render_with_generator( template::render_with_generator(
@ -114,7 +119,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {{
.build() .build()
}} }}
"#, "#,
name = options.plugin_name, name = plugin_name,
identifier = plugin_id identifier = plugin_id
); );

View File

@ -11,7 +11,7 @@ use anyhow::Context;
use clap::Parser; use clap::Parser;
use dialoguer::Input; use dialoguer::Input;
use handlebars::{to_json, Handlebars}; use handlebars::{to_json, Handlebars};
use heck::{AsKebabCase, ToKebabCase, ToPascalCase, ToSnakeCase}; use heck::{ToKebabCase, ToPascalCase, ToSnakeCase};
use include_dir::{include_dir, Dir}; use include_dir::{include_dir, Dir};
use log::warn; use log::warn;
use std::{ use std::{
@ -29,25 +29,34 @@ pub const TEMPLATE_DIR: Dir<'_> = include_dir!("templates/plugin");
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[clap(about = "Initializes a Tauri plugin project")] #[clap(about = "Initializes a Tauri plugin project")]
pub struct Options { pub struct Options {
/// Name of your Tauri plugin /// Name of your Tauri plugin.
#[clap(short = 'n', long = "name")] /// If not specified, it will be infered from the current directory.
plugin_name: String, pub(crate) plugin_name: Option<String>,
/// Initializes a Tauri plugin without the TypeScript API /// Initializes a Tauri plugin without the TypeScript API
#[clap(long)] #[clap(long)]
no_api: bool, pub(crate) no_api: bool,
/// Initializes a Tauri core plugin (internal usage) /// Initializes a Tauri core plugin (internal usage)
#[clap(long, hide(true))] #[clap(long, hide(true))]
tauri: bool, pub(crate) tauri: bool,
/// Set target directory for init /// Set target directory for init
#[clap(short, long)] #[clap(short, long)]
#[clap(default_value_t = current_dir().expect("failed to read cwd").display().to_string())] #[clap(default_value_t = current_dir().expect("failed to read cwd").display().to_string())]
directory: String, pub(crate) directory: String,
/// Path of the Tauri project to use (relative to the cwd) /// Path of the Tauri project to use (relative to the cwd)
#[clap(short, long)] #[clap(short, long)]
tauri_path: Option<PathBuf>, pub(crate) tauri_path: Option<PathBuf>,
/// Author name /// Author name
#[clap(short, long)] #[clap(short, long)]
author: Option<String>, pub(crate) author: Option<String>,
/// Whether to initialize an Android project for the plugin.
#[clap(long)]
pub(crate) android: bool,
/// Whether to initialize an iOS project for the plugin.
#[clap(long)]
pub(crate) ios: bool,
/// Whether to initialize Android and iOS projects for the plugin.
#[clap(long)]
pub(crate) mobile: bool,
} }
impl Options { impl Options {
@ -64,12 +73,15 @@ impl Options {
pub fn command(mut options: Options) -> Result<()> { pub fn command(mut options: Options) -> Result<()> {
options.load(); options.load();
let template_target_path = PathBuf::from(options.directory).join(format!(
"tauri-plugin-{}", let plugin_name = match options.plugin_name {
AsKebabCase(&options.plugin_name) None => super::infer_plugin_name(&options.directory)?,
)); Some(name) => name,
};
let template_target_path = PathBuf::from(options.directory);
let metadata = crates_metadata()?; let metadata = crates_metadata()?;
if template_target_path.exists() { if std::fs::read_dir(&template_target_path)?.count() > 0 {
warn!("Plugin dir ({:?}) not empty.", template_target_path); warn!("Plugin dir ({:?}) not empty.", template_target_path);
} else { } else {
let (tauri_dep, tauri_example_dep, tauri_build_dep) = let (tauri_dep, tauri_example_dep, tauri_build_dep) =
@ -101,7 +113,7 @@ pub fn command(mut options: Options) -> Result<()> {
handlebars.register_escape_fn(handlebars::no_escape); handlebars.register_escape_fn(handlebars::no_escape);
let mut data = BTreeMap::new(); let mut data = BTreeMap::new();
plugin_name_data(&mut data, &options.plugin_name); plugin_name_data(&mut data, &plugin_name);
data.insert("tauri_dep", to_json(tauri_dep)); data.insert("tauri_dep", to_json(tauri_dep));
data.insert("tauri_example_dep", to_json(tauri_example_dep)); data.insert("tauri_example_dep", to_json(tauri_example_dep));
data.insert("tauri_build_dep", to_json(tauri_build_dep)); data.insert("tauri_build_dep", to_json(tauri_build_dep));
@ -120,15 +132,20 @@ pub fn command(mut options: Options) -> Result<()> {
); );
} }
let plugin_id = request_input( let plugin_id = if options.android || options.mobile {
"What should be the Android Package ID for your plugin?", let plugin_id = request_input(
Some(format!("com.plugin.{}", options.plugin_name)), "What should be the Android Package ID for your plugin?",
false, Some(format!("com.plugin.{}", plugin_name)),
false, false,
)? false,
.unwrap(); )?
.unwrap();
data.insert("android_package_id", to_json(&plugin_id)); data.insert("android_package_id", to_json(&plugin_id));
Some(plugin_id)
} else {
None
};
let mut created_dirs = Vec::new(); let mut created_dirs = Vec::new();
template::render_with_generator( template::render_with_generator(
@ -157,13 +174,18 @@ pub fn command(mut options: Options) -> Result<()> {
} }
} }
"android" => { "android" => {
return generate_android_out_file( if options.android || options.mobile {
&path, return generate_android_out_file(
&template_target_path, &path,
&plugin_id.replace('.', "/"), &template_target_path,
&mut created_dirs, &plugin_id.as_ref().unwrap().replace('.', "/"),
); &mut created_dirs,
);
} else {
return Ok(None);
}
} }
"ios" if !(options.ios || options.mobile) => return Ok(None),
"webview-dist" | "webview-src" | "package.json" => { "webview-dist" | "webview-src" | "package.json" => {
if options.no_api { if options.no_api {
return Ok(None); return Ok(None);

View File

@ -29,15 +29,15 @@ pub struct Cli {
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
Add(AddOptions), Init(InitOptions),
} }
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[clap(about = "Adds the iOS project to an existing Tauri plugin")] #[clap(about = "Initializes the iOS project for an existing Tauri plugin")]
pub struct AddOptions { pub struct InitOptions {
/// Name of your Tauri plugin. Must match the current plugin's name. /// Name of your Tauri plugin. Must match the current plugin's name.
#[clap(short = 'n', long = "name")] /// If not specified, it will be infered from the current directory.
plugin_name: String, plugin_name: Option<String>,
/// The output directory. /// The output directory.
#[clap(short, long)] #[clap(short, long)]
#[clap(default_value_t = current_dir().expect("failed to read cwd").to_string_lossy().into_owned())] #[clap(default_value_t = current_dir().expect("failed to read cwd").to_string_lossy().into_owned())]
@ -46,7 +46,12 @@ pub struct AddOptions {
pub fn command(cli: Cli) -> Result<()> { pub fn command(cli: Cli) -> Result<()> {
match cli.command { match cli.command {
Commands::Add(options) => { Commands::Init(options) => {
let plugin_name = match options.plugin_name {
None => super::infer_plugin_name(std::env::current_dir()?)?,
Some(name) => name,
};
let out_dir = PathBuf::from(options.out_dir); let out_dir = PathBuf::from(options.out_dir);
if out_dir.join("ios").exists() { if out_dir.join("ios").exists() {
return Err(anyhow::anyhow!("ios folder already exists")); return Err(anyhow::anyhow!("ios folder already exists"));
@ -55,7 +60,7 @@ pub fn command(cli: Cli) -> Result<()> {
let handlebars = Handlebars::new(); let handlebars = Handlebars::new();
let mut data = BTreeMap::new(); let mut data = BTreeMap::new();
super::init::plugin_name_data(&mut data, &options.plugin_name); super::init::plugin_name_data(&mut data, &plugin_name);
let mut created_dirs = Vec::new(); let mut created_dirs = Vec::new();
template::render_with_generator( template::render_with_generator(
@ -111,7 +116,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {{
.build() .build()
}} }}
"#, "#,
name = options.plugin_name, name = plugin_name,
); );
log::info!("iOS project added"); log::info!("iOS project added");

View File

@ -0,0 +1,67 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::Result;
use clap::Parser;
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[clap(about = "Initializes a Tauri plugin project")]
pub struct Options {
/// Name of your Tauri plugin
plugin_name: String,
/// Initializes a Tauri plugin without the TypeScript API
#[clap(long)]
no_api: bool,
/// Initializes a Tauri core plugin (internal usage)
#[clap(long, hide(true))]
tauri: bool,
/// Set target directory for init
#[clap(short, long)]
directory: Option<String>,
/// Path of the Tauri project to use (relative to the cwd)
#[clap(short, long)]
tauri_path: Option<PathBuf>,
/// Author name
#[clap(short, long)]
author: Option<String>,
/// Whether to initialize an Android project for the plugin.
#[clap(long)]
android: bool,
/// Whether to initialize an iOS project for the plugin.
#[clap(long)]
ios: bool,
/// Whether to initialize Android and iOS projects for the plugin.
#[clap(long)]
mobile: bool,
}
impl From<Options> for super::init::Options {
fn from(o: Options) -> Self {
Self {
plugin_name: Some(o.plugin_name),
no_api: o.no_api,
tauri: o.tauri,
directory: o.directory.unwrap(),
tauri_path: o.tauri_path,
author: o.author,
android: o.android,
ios: o.ios,
mobile: o.mobile,
}
}
}
pub fn command(mut options: Options) -> Result<()> {
let cwd = std::env::current_dir()?;
if let Some(dir) = &options.directory {
std::fs::create_dir_all(cwd.join(dir))?;
} else {
let target = cwd.join(format!("tauri-plugin-{}", options.plugin_name));
std::fs::create_dir_all(&target)?;
options.directory.replace(target.display().to_string());
}
super::init::command(options.into())
}