mod server;
mod watcher;
use std::path::Path;
use std::sync::Arc;
use tokio::fs;
use tokio::sync::mpsc;
use crate::proc::template::PART_CONTEXT_PREFIX;
use crate::proc::{Context, ContextValue};
use crate::tool::procs::{ProcessorConfig, collect_assets, is_part, process_asset};
use crate::tool::{Config, ConfigProfile, DEFAULT_CONFIG_FILE, DEFAULT_CONFIG_PROFILE};
use std::collections::BTreeMap;
pub async fn run(port: u16, profile: Option<&str>) -> std::io::Result<()> {
let config = load_config(profile).await?;
let source_path = config.paths.get("source").ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"missing paths.source in config",
)
})?;
let target_path = config.paths.get("target").ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"missing paths.target in config",
)
})?;
let source = Path::new(source_path).to_path_buf();
let target = Path::new(target_path).to_path_buf();
tracing::info!("Running initial build...");
build(&source, &target, &config.procs, &config.context).await?;
let (rebuild_tx, mut rebuild_rx) = mpsc::channel::<()>(1);
let watcher_source = source.clone();
let _watcher = watcher::start(&watcher_source, rebuild_tx)?;
tracing::info!("Watching {} for changes", source.display());
let server_target = target.clone();
let server_handle = tokio::spawn(async move {
if let Err(e) = server::start(port, &server_target).await {
tracing::error!("Server error: {}", e);
}
});
tracing::info!("Server running at http://localhost:{}", port);
let config = Arc::new(config);
while rebuild_rx.recv().await.is_some() {
tracing::info!("Change detected, rebuilding...");
match build(&source, &target, &config.procs, &config.context).await {
Ok(()) => tracing::info!("Rebuild complete"),
Err(e) => tracing::error!("Rebuild failed: {}", e),
}
}
let _ = server_handle.await;
Ok(())
}
async fn load_config(profile: Option<&str>) -> std::io::Result<ConfigProfile> {
let config_path = Path::new(DEFAULT_CONFIG_FILE);
let profile_name = profile.unwrap_or(DEFAULT_CONFIG_PROFILE);
let config_toml = fs::read_to_string(config_path).await?;
let config: Config = toml::from_str(&config_toml).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid TOML: {}", e),
)
})?;
let default_profile = config.profiles.get(DEFAULT_CONFIG_PROFILE).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("missing default profile: {}", DEFAULT_CONFIG_PROFILE),
)
})?;
if profile_name == DEFAULT_CONFIG_PROFILE {
Ok(default_profile.clone())
} else {
let selected_profile = config.profiles.get(profile_name).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("missing selected profile: {}", profile_name),
)
})?;
Ok(default_profile.merge(selected_profile))
}
}
async fn build(
source: &Path,
target: &Path,
procs: &BTreeMap<String, ProcessorConfig>,
context_values: &BTreeMap<String, String>,
) -> std::io::Result<()> {
let mut assets = Vec::new();
collect_assets(source, &mut assets).await?;
if !fs::try_exists(target).await? {
fs::create_dir_all(target).await?;
}
let mut proc_context = Context::new();
for (key, value) in context_values {
proc_context.insert(key.clone().into(), ContextValue::Text(value.clone().into()));
}
proc_context.insert(
"_asset_source_root".into(),
ContextValue::Text(source.to_string_lossy().to_string().into()),
);
let mut regular_assets = Vec::new();
for (relative_path, content) in assets {
if is_part(&relative_path) {
let part_key = format!("{}{}", PART_CONTEXT_PREFIX, relative_path);
let content_str = String::from_utf8_lossy(&content).to_string();
proc_context.insert(part_key.into(), ContextValue::Text(content_str.into()));
tracing::debug!("Cached part: {}", relative_path);
} else {
regular_assets.push((relative_path, content));
}
}
let mut success_count = 0;
let mut error_count = 0;
for (relative_path, content) in regular_assets {
let result = process_asset(&relative_path, content, procs, &proc_context, target).await;
match result {
Ok(()) => success_count += 1,
Err(e) => {
tracing::error!("Error processing {}: {}", relative_path, e);
error_count += 1;
}
}
}
tracing::info!(
"Processed {} assets ({} errors)",
success_count,
error_count
);
Ok(())
}