use std::{collections::HashMap, fmt::Display, fs, io::Write, path, str::FromStr}; use anyhow::Context; use rust_embed::RustEmbed; use crate::{colors::*, manifest::Manifest, package_manager::PackageManager}; #[derive(RustEmbed)] #[folder = "fragments"] #[allow(clippy::upper_case_acronyms)] struct FRAGMENTS; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum Template { MakepadCounter, MakepadStackNavigation, MakepadLogin, Unknown, } impl Default for Template { fn default() -> Self { Template::MakepadCounter } } impl Template { pub const fn select_text<'a>(&self) -> &'a str { match self { Template::MakepadCounter => "Counter", Template::MakepadStackNavigation => "SimpleStackNavigation", Template::MakepadLogin => "SimpleLogin", Template::Unknown => "Unknown", _ => unreachable!(), } } } impl Display for Template { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Template::MakepadCounter => write!(f, "counter"), Template::MakepadStackNavigation => write!(f, "simplestacknavigation"), Template::MakepadLogin => write!(f, "simplelogin"), Template::Unknown => write!(f, "unknown"), } } } impl FromStr for Template { type Err = String; fn from_str(s: &str) -> Result { match s { "counter" => Ok(Template::MakepadCounter), "simplestacknavigation" => Ok(Template::MakepadStackNavigation), "simplelogin" => Ok(Template::MakepadLogin), _ => Err(format!( "{YELLOW}{s}{RESET} is not a valid template. Valid templates are [{}]", Template::ALL .iter() .map(|e| format!("{GREEN}{e}{RESET}")) .collect::>() .join(", ") )), } } } impl<'a> Template { pub const ALL: &'a [Template] = &[ Template::MakepadCounter, Template::MakepadStackNavigation, Template::MakepadLogin, ]; pub fn flavors<'b>(&self, pkg_manager: PackageManager) -> Option<&'b [Flavor]> { match self { Template::MakepadCounter => { if pkg_manager == PackageManager::Cargo { None } else { Some(&[Flavor::Unknown]) } } Template::MakepadStackNavigation => { if pkg_manager == PackageManager::Cargo { None } else { Some(&[Flavor::Unknown]) } } _ => None, } } pub fn from_flavor(&self, flavor: Flavor) -> Self { match (self, flavor) { (Template::Unknown, Flavor::Unknown) => Template::Unknown, _ => *self, } } pub fn without_flavor(&self) -> Self { match self { Template::Unknown => Template::Unknown, _ => *self, } } pub const fn possible_package_managers(&self) -> &[PackageManager] { match self { Template::MakepadCounter => &[PackageManager::Cargo,], Template::MakepadStackNavigation => &[PackageManager::Cargo], Template::MakepadLogin => &[PackageManager::Cargo], Template::Unknown => &[], } } pub const fn needs_trunk(&self) -> bool { matches!(self, Template::MakepadCounter | Template::MakepadStackNavigation | Template::MakepadLogin) } pub const fn needs_makepad_cli(&self) -> bool { matches!( self, Template::MakepadCounter | Template::MakepadStackNavigation | Template::MakepadLogin ) } pub const fn needs_wasm32_target(&self) -> bool { matches!(self, Template::MakepadCounter | Template::MakepadStackNavigation | Template::MakepadLogin) } pub fn render( &self, target_dir: &path::Path, pkg_manager: PackageManager, project_name: &str, package_name: &str, alpha: bool, mobile: bool, ) -> anyhow::Result<()> { let manifest_bytes = FRAGMENTS::get(&format!("fragment-{self}/_cta_manifest_")) .with_context(|| "Failed to get manifest bytes")? .data; let manifest_str = String::from_utf8(manifest_bytes.to_vec())?; let manifest = Manifest::parse(&manifest_str, mobile)?; let lib_name = format!("{}_lib", package_name.replace('-', "_")); let manifest_template_data: HashMap<&str, &str> = [ ("pkg_manager_run_command", pkg_manager.run_cmd()), ("lib_name", &lib_name), ("project_name", project_name), ("package_name", package_name), ( "double_dash_with_space", if pkg_manager == PackageManager::Unknown { "-- " } else { "" }, ), ] .into(); let template_data: HashMap<&str, String> = [ ("stable", (!alpha).to_string()), ("alpha", alpha.to_string()), ("mobile", mobile.to_string()), ("project_name", project_name.to_string()), ("package_name", package_name.to_string()), ( "before_dev_command", crate::lte::render( manifest.before_dev_command.unwrap_or_default(), &manifest_template_data, )?, ), ( "before_build_command", crate::lte::render( manifest.before_build_command.unwrap_or_default(), &manifest_template_data, )?, ), ( "dev_path", crate::lte::render( manifest.dev_path.unwrap_or_default(), &manifest_template_data, )?, ), ( "dist_dir", crate::lte::render( manifest.dist_dir.unwrap_or_default(), &manifest_template_data, )?, ), ( "with_global_makepad", manifest.with_global_makepad.unwrap_or_default().to_string(), ), ("lib_name", lib_name), ] .into(); let write_file = |file: &str, template_data| -> anyhow::Result<()> { // remove the first component, which is certainly the fragment directory they were in before getting embeded into the binary let p = path::PathBuf::from(file) .components() .skip(1) .collect::>() .iter() .collect::(); let p = target_dir.join(p); let file_name = p.file_name().unwrap().to_string_lossy(); let file_name = match &*file_name { "_gitignore" => ".gitignore", // skip manifest "_cta_manifest_" => return Ok(()), // conditional files: // are files that start with a special syntax // "%(%)" // flags are supported package managers, stable, alpha and mobile. // example: "%(pnpm-npm-yarn-stable-alpha)%package.json" name if name.starts_with("%(") && name[1..].contains(")%") => { let mut s = name.strip_prefix("%(").unwrap().split(")%"); let (mut flags, name) = ( s.next().unwrap().split('-').collect::>(), s.next().unwrap(), ); let for_stable = flags.contains(&"stable"); let for_alpha = flags.contains(&"alpha"); let for_mobile = flags.contains(&"mobile"); // remove these flags to only keep package managers flags flags.retain(|e| !["stable", "alpha", "mobile"].contains(e)); if ((for_stable && !alpha) || (for_alpha && alpha && !mobile) || (for_mobile && alpha && mobile) || (!for_stable && !for_alpha && !for_mobile)) && (flags.contains(&pkg_manager.to_string().as_str()) || flags.is_empty()) { name } else { // skip writing this file return Ok(()); } } name => name, }; // Only modify files that need to use the template engine let (file_data, file_name) = if let Some(file_name) = file_name.strip_suffix(".lts") { let file_data = FRAGMENTS::get(file).unwrap().data.to_vec(); let file_data_as_str = std::str::from_utf8(&file_data)?; ( crate::lte::render(file_data_as_str, template_data)?.into_bytes(), file_name, ) } else { (FRAGMENTS::get(file).unwrap().data.to_vec(), file_name) }; let parent = p.parent().unwrap(); fs::create_dir_all(parent)?; fs::write(parent.join(file_name), file_data)?; Ok(()) }; // 1. write base files for file in FRAGMENTS::iter().filter(|e| { path::PathBuf::from(e.to_string()) .components() .next() .unwrap() .as_os_str() == "_base_" }) { write_file(&file, &template_data)?; } // 2. write template files which can override files from base for file in FRAGMENTS::iter().filter(|e| { path::PathBuf::from(e.to_string()) .components() .next() .unwrap() .as_os_str() == path::PathBuf::from(format!("fragment-{self}")) }) { write_file(&file, &template_data)?; } // 3. write extra files specified in the fragment manifest for (src, dest) in manifest.files { let data = FRAGMENTS::get(&format!("_assets_/{src}")) .with_context(|| format!("Failed to get asset file bytes: {src}"))? .data; let dest = target_dir.join(dest); let parent = dest.parent().unwrap(); fs::create_dir_all(parent)?; let mut file = fs::OpenOptions::new() .append(true) .create(true) .open(dest)?; file.write_all(&data)?; } Ok(()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum Flavor { Unknown, } impl Display for Flavor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Flavor::Unknown => write!(f, "Unknown"), } } }