create-makepad-app/packages/cli/src/template.rs

335 lines
11 KiB
Rust

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<Self, Self::Err> {
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::<Vec<_>>()
.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::<Vec<_>>()
.iter()
.collect::<path::PathBuf>();
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
// "%(<list of flags separated by `-`>%)<file_name>"
// 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::<Vec<_>>(),
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"),
}
}
}