Browse Source

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

Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>
Amr Bashir 1 year ago
parent
commit
4caa1cca99

+ 12 - 0
.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`.

+ 32 - 0
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<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(),
+  )
+}

+ 14 - 9
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<String>,
   /// 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<R: Runtime>() -> TauriPlugin<R> {{
     .build()
 }}
 "#,
-        name = options.plugin_name,
+        name = plugin_name,
         identifier = plugin_id
       );
 

+ 51 - 29
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<String>,
   /// 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<PathBuf>,
+  pub(crate) tauri_path: Option<PathBuf>,
   /// Author name
   #[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 {
@@ -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);

+ 13 - 8
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<String>,
   /// 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<R: Runtime>() -> TauriPlugin<R> {{
     .build()
 }}
 "#,
-        name = options.plugin_name,
+        name = plugin_name,
       );
 
       log::info!("iOS project added");

+ 67 - 0
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<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())
+}