diff --git a/.changes/cli-plugin-init.md b/.changes/cli-plugin-init.md new file mode 100644 index 000000000..d250ad3f1 --- /dev/null +++ b/.changes/cli-plugin-init.md @@ -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`. diff --git a/tooling/cli/src/plugin.rs b/tooling/cli/src/plugin.rs index 2809e6432..481b85991 100644 --- a/tooling/cli/src/plugin.rs +++ b/tooling/cli/src/plugin.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use std::path::Path; + use clap::{Parser, Subcommand}; use crate::Result; @@ -9,6 +11,7 @@ use crate::Result; mod android; mod init; mod ios; +mod new; #[derive(Parser)] #[clap( @@ -25,6 +28,7 @@ pub struct Cli { #[derive(Subcommand)] enum Commands { + New(new::Options), Init(init::Options), Android(android::Cli), Ios(ios::Cli), @@ -32,6 +36,7 @@ enum Commands { pub fn command(cli: Cli) -> Result<()> { match cli.command { + Commands::New(options) => new::command(options)?, Commands::Init(options) => init::command(options)?, Commands::Android(cli) => android::command(cli)?, Commands::Ios(cli) => ios::command(cli)?, @@ -39,3 +44,30 @@ pub fn command(cli: Cli) -> Result<()> { Ok(()) } + +fn infer_plugin_name>(directory: P) -> Result { + 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(), + ) +} diff --git a/tooling/cli/src/plugin/android.rs b/tooling/cli/src/plugin/android.rs index 83df5e5a3..82bcfd4ef 100644 --- a/tooling/cli/src/plugin/android.rs +++ b/tooling/cli/src/plugin/android.rs @@ -28,15 +28,15 @@ pub struct Cli { #[derive(Subcommand)] enum Commands { - Add(AddOptions), + Init(InitOptions), } #[derive(Debug, Parser)] -#[clap(about = "Adds the Android project to an existing Tauri plugin")] -pub struct AddOptions { +#[clap(about = "Initializes the Android project for an existing Tauri plugin")] +pub struct InitOptions { /// Name of your Tauri plugin. Must match the current plugin's name. - #[clap(short = 'n', long = "name")] - plugin_name: String, + /// If not specified, it will be infered from the current directory. + plugin_name: Option, /// The output directory. #[clap(short, long)] #[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<()> { 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); if out_dir.join("android").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( "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, )? @@ -62,7 +67,7 @@ pub fn command(cli: Cli) -> Result<()> { let handlebars = Handlebars::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(); template::render_with_generator( @@ -114,7 +119,7 @@ pub fn init() -> TauriPlugin {{ .build() }} "#, - name = options.plugin_name, + name = plugin_name, identifier = plugin_id ); diff --git a/tooling/cli/src/plugin/init.rs b/tooling/cli/src/plugin/init.rs index 77cba7d37..6229dce62 100644 --- a/tooling/cli/src/plugin/init.rs +++ b/tooling/cli/src/plugin/init.rs @@ -11,7 +11,7 @@ use anyhow::Context; use clap::Parser; use dialoguer::Input; use handlebars::{to_json, Handlebars}; -use heck::{AsKebabCase, ToKebabCase, ToPascalCase, ToSnakeCase}; +use heck::{ToKebabCase, ToPascalCase, ToSnakeCase}; use include_dir::{include_dir, Dir}; use log::warn; use std::{ @@ -29,25 +29,34 @@ pub const TEMPLATE_DIR: Dir<'_> = include_dir!("templates/plugin"); #[derive(Debug, Parser)] #[clap(about = "Initializes a Tauri plugin project")] pub struct Options { - /// Name of your Tauri plugin - #[clap(short = 'n', long = "name")] - plugin_name: String, + /// Name of your Tauri plugin. + /// If not specified, it will be infered from the current directory. + pub(crate) plugin_name: Option, /// Initializes a Tauri plugin without the TypeScript API #[clap(long)] - no_api: bool, + pub(crate) no_api: bool, /// Initializes a Tauri core plugin (internal usage) #[clap(long, hide(true))] - tauri: bool, + pub(crate) tauri: bool, /// Set target directory for init #[clap(short, long)] #[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) #[clap(short, long)] - tauri_path: Option, + pub(crate) tauri_path: Option, /// Author name #[clap(short, long)] - author: Option, + pub(crate) author: Option, + /// 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 { @@ -64,12 +73,15 @@ impl Options { pub fn command(mut options: Options) -> Result<()> { options.load(); - let template_target_path = PathBuf::from(options.directory).join(format!( - "tauri-plugin-{}", - AsKebabCase(&options.plugin_name) - )); + + let plugin_name = match 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()?; - if template_target_path.exists() { + if std::fs::read_dir(&template_target_path)?.count() > 0 { warn!("Plugin dir ({:?}) not empty.", template_target_path); } else { 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); 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_example_dep", to_json(tauri_example_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( - "What should be the Android Package ID for your plugin?", - Some(format!("com.plugin.{}", options.plugin_name)), - false, - false, - )? - .unwrap(); + let plugin_id = if options.android || options.mobile { + let plugin_id = request_input( + "What should be the Android Package ID for your plugin?", + Some(format!("com.plugin.{}", plugin_name)), + false, + false, + )? + .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(); template::render_with_generator( @@ -157,13 +174,18 @@ pub fn command(mut options: Options) -> Result<()> { } } "android" => { - return generate_android_out_file( - &path, - &template_target_path, - &plugin_id.replace('.', "/"), - &mut created_dirs, - ); + if options.android || options.mobile { + return generate_android_out_file( + &path, + &template_target_path, + &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" => { if options.no_api { return Ok(None); diff --git a/tooling/cli/src/plugin/ios.rs b/tooling/cli/src/plugin/ios.rs index b40c890d0..1b44e3e3a 100644 --- a/tooling/cli/src/plugin/ios.rs +++ b/tooling/cli/src/plugin/ios.rs @@ -29,15 +29,15 @@ pub struct Cli { #[derive(Subcommand)] enum Commands { - Add(AddOptions), + Init(InitOptions), } #[derive(Debug, Parser)] -#[clap(about = "Adds the iOS project to an existing Tauri plugin")] -pub struct AddOptions { +#[clap(about = "Initializes the iOS project for an existing Tauri plugin")] +pub struct InitOptions { /// Name of your Tauri plugin. Must match the current plugin's name. - #[clap(short = 'n', long = "name")] - plugin_name: String, + /// If not specified, it will be infered from the current directory. + plugin_name: Option, /// The output directory. #[clap(short, long)] #[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<()> { 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); if out_dir.join("ios").exists() { return Err(anyhow::anyhow!("ios folder already exists")); @@ -55,7 +60,7 @@ pub fn command(cli: Cli) -> Result<()> { let handlebars = Handlebars::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(); template::render_with_generator( @@ -111,7 +116,7 @@ pub fn init() -> TauriPlugin {{ .build() }} "#, - name = options.plugin_name, + name = plugin_name, ); log::info!("iOS project added"); diff --git a/tooling/cli/src/plugin/new.rs b/tooling/cli/src/plugin/new.rs new file mode 100644 index 000000000..9e6a89c28 --- /dev/null +++ b/tooling/cli/src/plugin/new.rs @@ -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, + /// Path of the Tauri project to use (relative to the cwd) + #[clap(short, long)] + tauri_path: Option, + /// Author name + #[clap(short, long)] + author: Option, + /// 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 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()) +}