Skip to content

Commit

Permalink
Add start of CLI registry
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielleHuisman committed Jun 11, 2024
1 parent ce7f86f commit 0969229
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 13 deletions.
3 changes: 0 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
8 changes: 8 additions & 0 deletions packages/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
34 changes: 24 additions & 10 deletions packages/cli/src/bin/rust-shadcn-ui.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Error>> {
env_logger::init();

fn main() {
let matches = command!()
.propagate_version(true)
.subcommand_required(true)
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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::<String>("component")
.unwrap_or_default()
.map(|v| v.as_str())
.collect::<Vec<_>>()
),
.cloned()
.collect::<Vec<_>>(),
all: sub_matches.get_flag("all"),
cwd: sub_matches.get_one::<PathBuf>("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::<PathBuf>("path").cloned(),
yes: sub_matches.get_flag("yes"),
})?,
_ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
}

Ok(())
}
3 changes: 3 additions & 0 deletions packages/cli/src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod add;

pub use add::*;
52 changes: 52 additions & 0 deletions packages/cli/src/commands/add.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub yes: bool,
pub overwrite: bool,
pub cwd: PathBuf,
pub all: bool,
pub path: Option<PathBuf>,
}

pub fn add(options: AddOptions) -> Result<(), Box<dyn Error>> {
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(&registry_index, &selected_components);

Ok(())
} else {
Err("Configuration is missing. Please run `init` to create a components.toml file.".into())
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod commands;
pub mod utils;
2 changes: 2 additions & 0 deletions packages/cli/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod get_config;
pub mod registry;
62 changes: 62 additions & 0 deletions packages/cli/src/utils/get_config.rs
Original file line number Diff line number Diff line change
@@ -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<Config> {
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<RawConfig> {
// 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())
}
5 changes: 5 additions & 0 deletions packages/cli/src/utils/registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod index;
mod schema;

pub use index::*;
pub use schema::*;
85 changes: 85 additions & 0 deletions packages/cli/src/utils/registry/index.rs
Original file line number Diff line number Diff line change
@@ -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<RegistryItem> {
let results = fetch_registry::<Vec<RegistryItem>>(vec!["index.json".into()]).await;
results.first().expect("TODO")
}

pub fn resolve_tree(index: &Vec<RegistryItem>, names: &Vec<String>) -> Vec<RegistryItem> {
let mut tree: Vec<RegistryItem> = 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<RegistryItem>) {
let paths: Vec<String> = 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>,
) -> 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<T: DeserializeOwned + Send + 'static>(paths: Vec<String>) -> Vec<T> {
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![]
}
54 changes: 54 additions & 0 deletions packages/cli/src/utils/registry/schema.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub dev_dependencies: Vec<String>,
pub registry_dependencies: Vec<String>,
pub files: Vec<String>,
pub r#type: RegistryItemType,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RegistryItemWithContent {
pub name: String,
pub dependencies: Vec<String>,
pub dev_dependencies: Vec<String>,
pub registry_dependencies: Vec<String>,
pub files: Vec<RegistryItemFile>,
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,
}

0 comments on commit 0969229

Please sign in to comment.