diff --git a/Cargo.toml b/Cargo.toml index fd70533..6661803 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,6 @@ repository = "https://github.com/NixySoftware/shadcn-ui" version = "0.0.1" [workspace.dependencies] -console_log = "1.0.0" -console_error_panic_hook = "0.1.7" leptos = { version = "0.6.9", features = ["nightly"] } log = "0.4.21" tailwind_fuse = { version = "0.3.0", features = ["variant"] } -web-sys = "0.3.69" diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 5a5263b..ac5f606 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -10,3 +10,11 @@ version.workspace = true [dependencies] clap = { version = "4.5.4", features = ["cargo"] } +env_logger = "0.11.3" +futures = "0.3.30" +log.workspace = true +reqwest = { version = "0.12.4", features = ["gzip", "json"] } +serde = "1.0.203" +serde_json = "1.0.117" +tokio = { version = "1.38.0", features = ["full"] } +toml = "0.8.14" diff --git a/packages/cli/src/bin/rust-shadcn-ui.rs b/packages/cli/src/bin/rust-shadcn-ui.rs index 644203b..2307e7d 100644 --- a/packages/cli/src/bin/rust-shadcn-ui.rs +++ b/packages/cli/src/bin/rust-shadcn-ui.rs @@ -1,6 +1,12 @@ -use clap::{command, Arg, ArgAction, Command}; +use std::{env, error::Error, path::PathBuf}; + +use clap::{command, value_parser, Arg, ArgAction, Command}; +use shadcn_ui_cli::commands::{add, AddOptions}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); -fn main() { let matches = command!() .propagate_version(true) .subcommand_required(true) @@ -24,7 +30,8 @@ fn main() { Arg::new("cwd") .short('c') .long("cwd") - .help("The working directory, defaults to the current directory"), + .help("The working directory, defaults to the current directory") + .value_parser(value_parser!(PathBuf)), ) .arg( Arg::new("overwrite") @@ -37,7 +44,8 @@ fn main() { Arg::new("path") .short('p') .long("path") - .help("The path to add the component to"), + .help("The path to add the component to") + .value_parser(value_parser!(PathBuf)), ) .arg( Arg::new("yes") @@ -68,14 +76,20 @@ fn main() { .get_matches(); match matches.subcommand() { - Some(("add", sub_matches)) => println!( - "'myapp add' was used, name is: {:?}", - sub_matches + Some(("add", sub_matches)) => add(AddOptions { + components: sub_matches .get_many::("component") .unwrap_or_default() - .map(|v| v.as_str()) - .collect::>() - ), + .cloned() + .collect::>(), + all: sub_matches.get_flag("all"), + cwd: sub_matches.get_one::("cwd").cloned().unwrap_or(env::current_dir().expect("Current directory does not exist or there are insufficient permissions to access it.")), + overwrite: sub_matches.get_flag("overwrite"), + path: sub_matches.get_one::("path").cloned(), + yes: sub_matches.get_flag("yes"), + })?, _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), } + + Ok(()) } diff --git a/packages/cli/src/commands.rs b/packages/cli/src/commands.rs new file mode 100644 index 0000000..f838ad1 --- /dev/null +++ b/packages/cli/src/commands.rs @@ -0,0 +1,3 @@ +mod add; + +pub use add::*; diff --git a/packages/cli/src/commands/add.rs b/packages/cli/src/commands/add.rs new file mode 100644 index 0000000..18d440d --- /dev/null +++ b/packages/cli/src/commands/add.rs @@ -0,0 +1,52 @@ +use std::{error::Error, path::PathBuf}; + +use crate::utils::{ + get_config::get_config, + registry::{get_registry_index, resolve_tree}, +}; + +#[derive(Debug)] +pub struct AddOptions { + pub components: Vec, + pub yes: bool, + pub overwrite: bool, + pub cwd: PathBuf, + pub all: bool, + pub path: Option, +} + +pub fn add(options: AddOptions) -> Result<(), Box> { + println!("{:?}", options); + + if !options.cwd.exists() { + return Err(format!( + "Path {} does not exist. Please try again.", + options.cwd.display() + ) + .into()); + } + + if let Some(config) = get_config(&options.cwd) { + let registry_index = get_registry_index(); + + let selected_components = match options.all { + true => registry_index + .iter() + .map(|item| item.name.clone()) + .collect(), + false => options.components, + }; + + // TODO: prompt + + if selected_components.is_empty() { + return Err("No components selected.".into()); + } + + let tree = resolve_tree(®istry_index, &selected_components); + + Ok(()) + } else { + Err("Configuration is missing. Please run `init` to create a components.toml file.".into()) + } +} diff --git a/packages/cli/src/lib.rs b/packages/cli/src/lib.rs index e69de29..52ba7ca 100644 --- a/packages/cli/src/lib.rs +++ b/packages/cli/src/lib.rs @@ -0,0 +1,2 @@ +pub mod commands; +pub mod utils; diff --git a/packages/cli/src/utils.rs b/packages/cli/src/utils.rs new file mode 100644 index 0000000..263181d --- /dev/null +++ b/packages/cli/src/utils.rs @@ -0,0 +1,2 @@ +pub mod get_config; +pub mod registry; diff --git a/packages/cli/src/utils/get_config.rs b/packages/cli/src/utils/get_config.rs new file mode 100644 index 0000000..1cb5c77 --- /dev/null +++ b/packages/cli/src/utils/get_config.rs @@ -0,0 +1,62 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RawConfig { + style: String, + tailwind: TailwindConfig, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TailwindConfig { + config: String, + css: String, + base_color: String, + css_variables: bool, + prefix: String, +} + +#[derive(Clone, Debug)] +pub struct Config { + pub style: String, + pub tailwind: TailwindConfig, + pub resolved_paths: ResolvedPaths, +} + +#[derive(Clone, Debug)] +pub struct ResolvedPaths { + pub tailwind_config: PathBuf, + pub tailwind_css: PathBuf, +} + +pub fn get_config(cwd: &Path) -> Option { + get_raw_config(cwd).map(|config| resolve_config_paths(cwd, config)) +} + +pub fn resolve_config_paths(cwd: &Path, config: RawConfig) -> Config { + Config { + style: config.style, + tailwind: config.tailwind.clone(), + resolved_paths: ResolvedPaths { + tailwind_config: cwd.join(config.tailwind.config), + tailwind_css: cwd.join(config.tailwind.css), + }, + } +} + +pub fn get_raw_config(cwd: &Path) -> Option { + // TODO: use search algorithm + let config_path = cwd.join("components.toml"); + + if !config_path.exists() { + return None; + } + + fs::read_to_string(config_path) + .ok() + .and_then(|config_content| toml::from_str(&config_content).ok()) +} diff --git a/packages/cli/src/utils/registry.rs b/packages/cli/src/utils/registry.rs new file mode 100644 index 0000000..31ef86f --- /dev/null +++ b/packages/cli/src/utils/registry.rs @@ -0,0 +1,5 @@ +mod index; +mod schema; + +pub use index::*; +pub use schema::*; diff --git a/packages/cli/src/utils/registry/index.rs b/packages/cli/src/utils/registry/index.rs new file mode 100644 index 0000000..96f5591 --- /dev/null +++ b/packages/cli/src/utils/registry/index.rs @@ -0,0 +1,85 @@ +use std::error::Error; + +use futures::{stream, StreamExt}; +use reqwest::Response; +use serde::de::DeserializeOwned; +use serde::Deserialize; + +use crate::utils::get_config::Config; +use crate::utils::registry::schema::{RegistryItem, RegistryItemType}; + +const BASE_URL: &str = "https://ui.shadcn.com"; + +pub async fn get_registry_index() -> Vec { + let results = fetch_registry::>(vec!["index.json".into()]).await; + results.first().expect("TODO") +} + +pub fn resolve_tree(index: &Vec, names: &Vec) -> Vec { + let mut tree: Vec = vec![]; + + for name in names { + if let Some(item) = index.iter().find(|item| item.name == *name) { + tree.push(item.clone()); + + if !item.registry_dependencies.is_empty() { + tree.append(&mut resolve_tree(index, &item.registry_dependencies)); + } + } + } + + tree.iter() + .enumerate() + .filter(|&(index, item)| { + tree.iter() + .position(|i| i.name == item.name) + .is_some_and(|position| position == index) + }) + .map(|(_, component)| component) + .cloned() + .collect() +} + +pub fn fetch_tree(style: String, tree: Vec) { + let paths: Vec = tree + .iter() + .map(|item| format!("styles/{}/{}.json", style, item.name)) + .collect(); + + let result = fetch_registry(paths); +} + +pub fn get_item_target_path( + config: Config, + item: RegistryItem, + r#override: Option, +) -> String { + if let Some(r#override) = r#override { + return r#override; + } + + // if (item.r#type == RegistryItemType::Ui) { + // return config.resolved_paths.ui + // } + + todo!("get_item_target_path") +} + +async fn fetch_registry(paths: Vec) -> Vec { + let client = reqwest::Client::new(); + + let responses = stream::iter(paths) + .map(|path| { + let client = client.clone(); + tokio::spawn(async move { + let response = client + .get(format!("{}/registry/{}", BASE_URL, path)) + .send() + .await?; + response.bytes().await + }) + }) + .buffer_unordered(10); + + vec![] +} diff --git a/packages/cli/src/utils/registry/schema.rs b/packages/cli/src/utils/registry/schema.rs new file mode 100644 index 0000000..d973099 --- /dev/null +++ b/packages/cli/src/utils/registry/schema.rs @@ -0,0 +1,54 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub enum RegistryItemType { + Example, + Component, + Ui, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RegistryItem { + pub name: String, + pub dependencies: Vec, + pub dev_dependencies: Vec, + pub registry_dependencies: Vec, + pub files: Vec, + pub r#type: RegistryItemType, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RegistryItemWithContent { + pub name: String, + pub dependencies: Vec, + pub dev_dependencies: Vec, + pub registry_dependencies: Vec, + pub files: Vec, + pub r#type: RegistryItemType, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RegistryItemFile { + pub name: String, + pub content: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RegistryStyle { + pub name: String, + pub label: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RegistryBaseColor { + inline_colors: RegistryBaseColorThemes, + css_vars: RegistryBaseColorThemes, + inline_colors_template: String, + css_vars_template: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RegistryBaseColorThemes { + pub light: String, + pub dark: String, +}