use crate::LocalStarterGroupLookup;
use glob::glob;
use log::debug;
use log::error;
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PreviewConfig {
pub template: Option<String>,
pub dependencies: Option<HashMap<String, String>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct StarterConfig {
pub description: Option<String>,
#[serde(rename = "defaultDir")]
pub default_dir: Option<PathBuf>,
#[serde(rename = "mainFile")]
pub main_file: Option<String>,
pub preview: Option<PreviewConfig>,
}
impl FromStr for StarterConfig {
type Err = serde_yaml::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_yaml::from_str(s)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LocalStarterFile {
pub path: String,
pub contents: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RemoteStarter {
pub github_username: String,
pub github_repo: String,
pub group: String,
pub name: String,
}
impl RemoteStarter {
pub fn new(github_username: &str, github_repo: &str, group: &str, name: &str) -> Self {
Self {
github_username: github_username.to_string(),
github_repo: github_repo.to_string(),
group: group.to_string(),
name: name.to_string(),
}
}
pub fn from_path(path: &str) -> Option<Self> {
let parts: Vec<&str> = path.split('/').collect();
let github_username = &parts[0][1..];
match parts.len() {
3 => {
let github_repo = "jump-start";
Some(Self::new(github_username, github_repo, parts[1], parts[2]))
}
4 => {
let github_repo = parts[1];
Some(Self::new(github_username, github_repo, parts[2], parts[3]))
}
_ => panic!("Could not parse remote starter from string {:?}", path),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LocalStarter {
pub path: String,
pub group: String,
pub name: String,
pub config: Option<StarterConfig>,
}
impl LocalStarter {
pub fn new(group: &str, name: &str) -> Self {
let path = format!("{}/{}", group, name);
Self {
group: group.to_string(),
name: name.to_string(),
path,
config: None,
}
}
pub fn from_path(path: &str) -> Option<Self> {
let parts: Vec<&str> = path.split('/').collect();
if parts.len() != 2 {
return None;
}
Some(Self::new(parts[0], parts[1]))
}
}
pub fn parse_starters(path: &Path) -> io::Result<LocalStarterGroupLookup> {
let mut groups: LocalStarterGroupLookup = HashMap::new();
let pattern = format!("{}/**/*jump-start.yaml", path.display());
for entry in glob(&pattern).expect("Failed to read glob pattern") {
match entry {
Ok(path) => {
let path_str = path.to_string_lossy();
if path_str.contains("node_modules") || path_str.contains("jump-start-tools") {
continue;
}
let file_content = fs::read_to_string(&path)?;
debug!("Parsing YAML file: {}", path.display());
debug!("Content: {}", file_content);
let starter_config = match file_content.parse::<StarterConfig>() {
Ok(config) => config,
Err(e) => {
error!("Error parsing yaml for {}: {}", path.display(), e);
continue;
}
};
let current_dir = path.parent().unwrap();
let name = current_dir
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
let group = current_dir
.parent()
.unwrap()
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
let starter = LocalStarter {
name: name.clone(),
group: group.clone(),
path: format!("{}/{}", group, name),
config: Some(starter_config),
};
groups.entry(group).or_default().push(starter);
}
Err(e) => error!("Error processing glob entry: {}", e),
}
}
Ok(groups)
}
pub fn get_starter_files(
starter: &LocalStarter,
instance_dir: &Path,
) -> io::Result<Vec<LocalStarterFile>> {
let mut out = Vec::new();
let excluded_files = ["jump-start.yaml", "degit.json"];
let starter_dir = instance_dir.join(&starter.group).join(&starter.name);
if starter_dir.exists() && starter_dir.is_dir() {
visit_dirs(
&starter_dir,
&mut out,
&excluded_files,
&starter_dir.to_string_lossy(),
)?;
debug!(
"Found {} files for starter {}/{}",
out.len(),
starter.group,
starter.name
);
} else {
error!("Warning: Starter directory not found: {:?}", starter_dir);
out.push(LocalStarterFile {
path: "example.file".to_string(),
contents: "// This is a sample file content\nconsole.log('Hello world');\n".to_string(),
});
}
Ok(out)
}
fn visit_dirs(
dir: &Path,
files: &mut Vec<LocalStarterFile>,
excluded_files: &[&str],
base_path: &str,
) -> io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
visit_dirs(&path, files, excluded_files, base_path)?;
} else if let Some(file_name) = path.file_name() {
let file_name_str = file_name.to_string_lossy();
if !excluded_files.contains(&file_name_str.as_ref()) {
let base = Path::new(base_path);
let rel_path = match path.strip_prefix(base) {
Ok(rel) => rel.to_string_lossy().to_string(),
Err(_) => path.to_string_lossy().to_string(),
};
match fs::read_to_string(&path) {
Ok(contents) => {
files.push(LocalStarterFile {
path: rel_path,
contents,
});
}
Err(e) => {
error!("Warning: Could not read file {:?}: {}", path, e);
}
}
}
}
}
}
Ok(())
}
pub fn get_starter_command(
starter: &LocalStarter,
github_username: &str,
github_repo: &str,
degit_mode: &str,
) -> String {
if degit_mode == "true" {
format!(
"npx degit {}/{}#{}/{} {}",
github_username, github_repo, starter.group, starter.name, starter.name
)
} else {
format!("jump-start add {}/{}", starter.group, starter.name)
}
}