use crate::{BASE_SERVER_TOML, PercentageDisplay, generate, save_inline_hashes};
use anyhow::bail;
use fs_err::{File, create_dir_all};
use ordinary_build_utils::html;
use ordinary_config::{
ContentDefinition, ModelConfig, OrdinaryConfig, TemplateConfig, TemplateFfiSerialization,
};
use std::collections::BTreeMap;
use std::env::set_current_dir;
use std::io::Write;
use std::path::Path;
use std::process::Command;
pub enum CompileResult {
Continue,
Done,
}
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]
pub fn run(
no_cache: bool,
generator: &str,
project_dir: &Path,
template_config: &TemplateConfig,
config: &OrdinaryConfig,
server_dir_path: &Path,
templates_dir_path: &Path,
hashes_dir_path: &Path,
content_def_map: &mut BTreeMap<String, ContentDefinition>,
model_config_map: &mut BTreeMap<String, ModelConfig>,
) -> anyhow::Result<CompileResult> {
let template_mime = template_config.mime_validated();
let template_name = template_config.name_validated();
if let Some(template_path) = &template_config.path {
let path = project_dir.join(template_path);
let mut template_string = fs_err::read_to_string(&path)?;
template_string = template_string.replace("{{ version }}", &config.version);
if let Some(vars) = &template_config.variables {
for var in vars {
let env_var = std::env::var(var)?;
template_string = template_string.replace(&format!("{{{{ {var} }}}}"), &env_var);
}
}
let (template_final, csp_hashes) = if template_config.minify == Some(true) {
if template_mime == "text/html" || template_mime == "text/html; charset=utf-8" {
let html_string = html::minify(&template_string)?;
let csp_hashes =
save_inline_hashes(html_string.clone(), hashes_dir_path, template_name);
(
html_string.as_bytes().to_vec(),
csp_hashes.has_any().then_some(csp_hashes),
)
} else {
(template_string.as_bytes().to_vec(), None)
}
} else if template_mime == "text/html" || template_mime == "text/html; charset=utf-8" {
let csp_hashes =
save_inline_hashes(template_string.clone(), hashes_dir_path, template_name);
(
template_string.as_bytes().to_vec(),
csp_hashes.has_any().then_some(csp_hashes),
)
} else {
(template_string.as_bytes().to_vec(), None)
};
let file_name = match path.extension() {
Some(ext) => match ext.to_str() {
Some(ext) => &format!("{template_name}.{ext}"),
None => template_name,
},
None => template_name,
};
let mut global_vars = BTreeMap::new();
if let Some(globals) = &config.globals {
for global_var in globals {
global_vars.insert(global_var.name.clone(), global_var.clone());
}
}
let server = match template_config.ffi_validated().serialization {
TemplateFfiSerialization::FlexBufferVector => {
generate::template::v1::flexbuffer_vector::generate_template_renderers(
&config.domain,
&config.version,
&config.canonical.clone().unwrap_or(
config
.cnames
.clone()
.unwrap_or(vec![config.domain.clone()])
.first()
.unwrap_or(&config.domain)
.clone(),
),
generator,
file_name,
template_config,
content_def_map,
model_config_map,
&global_vars,
config.error.as_ref(),
config.auth.as_ref(),
)
}
TemplateFfiSerialization::Json => {
bail!("Json serialization is not supported for V1 FFI templates");
}
};
let file_path = templates_dir_path.join(file_name);
if !no_cache && file_path.exists() && fs_err::read(&file_path)? == template_final {
tracing::info!(
name = template_config.name,
"template has not changed; skipping."
);
return Ok(CompileResult::Continue);
}
let mut file = File::create(file_path)?;
file.write_all(&template_final)?;
file.flush()?;
create_dir_all(server_dir_path.join(template_name).join("src"))?;
let server_main_rs = server_dir_path
.join(template_name)
.join("src")
.join("main.rs");
let mut server_main_rs_file = File::create(server_main_rs)?;
server_main_rs_file.write_all(server.as_bytes())?;
server_main_rs_file.flush()?;
let server_cargo = server_dir_path.join(template_name).join("Cargo.toml");
let mut server_cargo_file = File::create(server_cargo)?;
server_cargo_file.write_all(BASE_SERVER_TOML.as_bytes())?;
server_cargo_file.flush()?;
let server_askama = server_dir_path.join(template_name).join("askama.toml");
let mut server_askama_file = File::create(server_askama)?;
server_askama_file.write_all(
r#"# generated
[general]
dirs = ["../../templates"]
[[escaper]]
path = "askama::filters::Text"
extensions = ["js", "json"]
"#
.as_bytes(),
)?;
server_askama_file.flush()?;
let path = server_dir_path.join(template_name);
set_current_dir(path)?;
Command::new("cargo").args(["fmt"]).output()?;
let output = Command::new("cargo")
.args(["build", "--release", "--target", "wasm32-wasip1"])
.output()?;
if output.status.success() {
let template_wasm_path = "target/wasm32-wasip1/release/template.wasm";
let template_bytes = fs_err::read(template_wasm_path)?;
if template_config.minify == Some(true) {
tracing::info!(
name = template_config.name,
mime = template_config.mime,
size.bin = %bytesize::ByteSize(template_bytes.len() as u64)
.display()
.si(),
size.source = %bytesize::ByteSize(template_string.len() as u64)
.display()
.si(),
size.minified = %bytesize::ByteSize(template_final.len() as u64)
.display()
.si(),
size.reduction = %PercentageDisplay(((template_string.len() as f64 - template_final.len() as f64)
/ template_string.len() as f64)
* 100.0),
csp = csp_hashes.map(display),
"template"
);
} else {
tracing::info!(
name = template_config.name,
mime = template_config.mime,
size.bin = %bytesize::ByteSize(template_bytes.len() as u64)
.display()
.si(),
size.source = %bytesize::ByteSize(template_string.len() as u64)
.display()
.si(),
csp = csp_hashes.map(display),
"template"
);
}
} else {
tracing::error!(
name = template_config.name,
"failed for template\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
}
set_current_dir(project_dir)?;
Ok(CompileResult::Done)
}