diff --git a/crates/turborepo-lib/src/cli.rs b/crates/turborepo-lib/src/cli.rs index b7d5806bade68..48b01f95ce7e4 100644 --- a/crates/turborepo-lib/src/cli.rs +++ b/crates/turborepo-lib/src/cli.rs @@ -14,7 +14,7 @@ use tracing::{debug, error}; use turbopath::AbsoluteSystemPathBuf; use crate::{ - commands::{bin, daemon, link, login, logout, unlink, CommandBase}, + commands::{bin, daemon, generate, link, login, logout, unlink, CommandBase}, get_version, shim::{RepoMode, RepoState}, tracing::TurboSubscriber, @@ -264,6 +264,14 @@ pub enum Command { #[clap(long, value_enum, default_value_t = LinkTarget::RemoteCache)] target: LinkTarget, }, + /// Generate a new app / package + Generate { + #[clap(long, default_value_t = String::from("latest"), hide = true)] + tag: String, + #[clap(subcommand)] + #[serde(flatten)] + command: GenerateCommand, + }, /// Login to your Vercel account Login { #[clap(long = "sso-team")] @@ -300,6 +308,66 @@ pub enum Command { }, } +#[derive(Parser, Clone, Debug, Default, Serialize, PartialEq)] +pub struct GenerateCustomArgs { + /// The name of the generator to run + pub generator_name: Option, + /// Generator configuration file + #[clap(short = 'c', long)] + pub config: Option, + /// The root of your repository (default: directory with root turbo.json) + #[clap(short = 'r', long)] + pub root: Option, + /// Answers passed directly to generator + #[clap(short = 'a', long, value_delimiter = ' ', num_args = 1..)] + pub args: Vec, +} + +#[derive(Parser, Clone, Debug, Default, Serialize, PartialEq)] +pub struct GenerateAddArgs { + /// Name for the new workspace + #[clap(short = 'n', long)] + pub name: Option, + /// Generate an empty workspace + #[clap(short = 'b', long, conflicts_with = "copy", default_value_t = true)] + pub empty: bool, + /// Generate a workspace using an existing workspace as a template + #[clap(short = 'c', long, conflicts_with = "empty", default_value_t = false)] + pub copy: bool, + /// Where the new workspace should be created + #[clap(short = 'd', long)] + pub destination: Option, + /// The type of workspace to create + #[clap(short = 'w', long)] + pub what: Option, + /// The root of your repository (default: directory with root turbo.json) + #[clap(short = 'r', long)] + pub root: Option, + /// An example package to add. You can use a GitHub URL with any branch + /// and/or subdirectory. + #[clap(short = 'e', long)] + pub example: Option, + /// In a rare case, your GitHub URL might contain a branch name with a slash + /// (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). In this + /// case, you must specify the path to the example separately: + /// --example-path foo/bar + #[clap(short = 'p', long)] + pub example_path: Option, + /// Do not filter available dependencies by the workspace type + #[clap(long, default_value_t = false)] + pub show_all_dependencies: bool, +} + +#[derive(Subcommand, Clone, Debug, Serialize, PartialEq)] +pub enum GenerateCommand { + /// Add a new package or app to your project + #[clap(name = "add", alias = "a")] + Add(GenerateAddArgs), + /// Run custom generators + #[clap(name = "run", alias = "r")] + Custom(GenerateCustomArgs), +} + #[derive(Parser, Clone, Debug, Default, Serialize, PartialEq)] pub struct RunArgs { /// Override the filesystem cache directory. @@ -567,6 +635,10 @@ pub async fn run( Ok(Payload::Rust(Ok(0))) } + Command::Generate { command, tag } => { + generate::run(command, tag)?; + Ok(Payload::Rust(Ok(0))) + } Command::Daemon { command, idle_time } => { let base = CommandBase::new(cli_args.clone(), repo_root, version, ui)?; diff --git a/crates/turborepo-lib/src/commands/generate.rs b/crates/turborepo-lib/src/commands/generate.rs new file mode 100644 index 0000000000000..2603a74fd5fea --- /dev/null +++ b/crates/turborepo-lib/src/commands/generate.rs @@ -0,0 +1,54 @@ +use std::process::{Command, Stdio}; + +use anyhow::Result; + +use crate::{child::spawn_child, cli::GenerateCommand}; + +fn verify_requirements() -> Result<()> { + let output = Command::new("npx") + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + match output { + Ok(result) if result.success() => Ok(()), + _ => Err(anyhow::anyhow!( + "Unable to run generate - missing requirements (npx)" + )), + } +} + +fn call_turbo_gen(command: &str, tag: &String, raw_args: &str) -> Result { + let mut npx = Command::new("npx"); + npx.arg("--yes") + .arg(format!("@turbo/gen@{}", tag)) + .arg("raw") + .arg(command) + .args(["--json", raw_args]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + + let child = spawn_child(npx)?; + let exit_code = child.wait()?.code().unwrap_or(2); + Ok(exit_code) +} + +pub fn run(command: &GenerateCommand, tag: &String) -> Result<()> { + // ensure npx is available + verify_requirements()?; + + match command { + GenerateCommand::Add(args) => { + // convert args to json + let raw_args = serde_json::to_string(args)?; + call_turbo_gen("add", tag, &raw_args)?; + } + GenerateCommand::Custom(args) => { + let raw_args = serde_json::to_string(args)?; + call_turbo_gen("generate", tag, &raw_args)?; + } + }; + + Ok(()) +} diff --git a/crates/turborepo-lib/src/commands/mod.rs b/crates/turborepo-lib/src/commands/mod.rs index bab5ecd8cb7c6..f8443dd757263 100644 --- a/crates/turborepo-lib/src/commands/mod.rs +++ b/crates/turborepo-lib/src/commands/mod.rs @@ -17,6 +17,7 @@ use crate::{ pub(crate) mod bin; pub(crate) mod daemon; +pub(crate) mod generate; pub(crate) mod link; pub(crate) mod login; pub(crate) mod logout; diff --git a/turborepo-tests/integration/tests/no_args.t b/turborepo-tests/integration/tests/no_args.t index f4f5e628bd5bf..752a6ee9d3ec7 100644 --- a/turborepo-tests/integration/tests/no_args.t +++ b/turborepo-tests/integration/tests/no_args.t @@ -12,6 +12,7 @@ Make sure exit code is 2 when no args are passed completion Generate the autocompletion script for the specified shell daemon Runs the Turborepo background daemon link Link your local directory to a Vercel organization and enable remote caching + generate Generate a new app / package login Login to your Vercel account logout Logout to your Vercel account prune Prepare a subset of your monorepo diff --git a/turborepo-tests/integration/tests/turbo_help.t b/turborepo-tests/integration/tests/turbo_help.t index 8c448c355fa73..2d20f57e34370 100644 --- a/turborepo-tests/integration/tests/turbo_help.t +++ b/turborepo-tests/integration/tests/turbo_help.t @@ -12,6 +12,7 @@ Test help flag completion Generate the autocompletion script for the specified shell daemon Runs the Turborepo background daemon link Link your local directory to a Vercel organization and enable remote caching + generate Generate a new app / package login Login to your Vercel account logout Logout to your Vercel account prune Prepare a subset of your monorepo @@ -78,6 +79,7 @@ Test help flag completion Generate the autocompletion script for the specified shell daemon Runs the Turborepo background daemon link Link your local directory to a Vercel organization and enable remote caching + generate Generate a new app / package login Login to your Vercel account logout Logout to your Vercel account prune Prepare a subset of your monorepo