use clap::{Args, Parser, Subcommand};
use std::fs;
use std::path::Path;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
#[command(args_conflicts_with_subcommands = true)]
#[command(flatten_help = true)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[command(flatten)]
build_args: BuildArgs,
}
#[derive(Subcommand, Debug)]
enum Commands {
Build(BuildArgs),
Init,
}
#[derive(Args, Debug, Clone)]
struct BuildArgs {
#[arg(short, long)]
source: Option<String>,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long)]
transform_script: Option<String>,
#[arg(long, hide = true)]
js_script: Option<String>,
#[arg(short, long)]
escape_func: Option<String>,
#[arg(short, long)]
config: Option<String>,
}
impl BuildArgs {
fn resolve_transform_script(&self) -> Option<&str> {
self.transform_script
.as_deref()
.or(self.js_script.as_deref())
}
}
fn main() {
let cli = Cli::parse();
let result = match cli.command {
Some(Commands::Build(args)) => run_build(args),
Some(Commands::Init) => run_init(),
None => run_build(cli.build_args),
};
if let Err(e) = result {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
fn run_init() -> Result<(), Box<dyn std::error::Error>> {
let formefile_ts = Path::new("formefile.ts");
let formefile_js = Path::new("formefile.js");
if formefile_ts.exists() {
return Err("formefile.ts already exists".into());
}
if formefile_js.exists() {
return Err("formefile.js already exists".into());
}
let content = r#"// formefile.ts — forme template compiler configuration
// Run `forme build` or just `forme` to compile templates.
interface FormeContext {
cwd: string;
env: Record<string, string>;
cli: {
source?: string;
output?: string;
transform_script?: string;
escape_func?: string;
};
}
interface FormeConfig {
source: string;
output: string;
transform_script?: string;
escape_func?: string;
}
export function config(ctx: FormeContext): FormeConfig | FormeConfig[] {
return {
// Source template directory
source: "templates",
// Output generated Rust file
output: "src/generated.rs",
// Optional: JS/TS transform script for custom elements
// transform_script: "src/components.js",
// Optional: custom escape function (default: "forme::html_escape")
// escape_func: "my_crate::escape",
};
}
"#;
fs::write(formefile_ts, content)?;
println!("Created formefile.ts");
Ok(())
}
fn run_build(args: BuildArgs) -> Result<(), Box<dyn std::error::Error>> {
let config_file = forme::discover_config_file(args.config.as_deref());
if let Some(config_path) = config_file {
println!("Using config: {}", config_path.display());
let configs = forme::evaluate_config_file(
&config_path,
args.source.as_deref(),
args.output.as_deref(),
args.resolve_transform_script(),
args.escape_func.as_deref(),
)?;
if configs.is_empty() {
println!("Config returned no targets, nothing to do.");
return Ok(());
}
for config in &configs {
forme::Builder::new(&config.source, &config.output)
.transform_script_option(config.transform_script.as_deref())
.escape_func_option(config.escape_func.as_deref())
.build()?;
}
Ok(())
} else {
let source = args.source.as_deref().ok_or(
"Missing required argument: --source (or provide a formefile.ts/formefile.js config file)",
)?;
let output = args.output.as_deref().ok_or(
"Missing required argument: --output (or provide a formefile.ts/formefile.js config file)",
)?;
let mut builder = forme::Builder::new(source, output);
if let Some(ts) = args.resolve_transform_script() {
builder = builder.transform_script(ts);
}
if let Some(ef) = args.escape_func.as_deref() {
builder = builder.escape_func(ef);
}
builder.build()?;
Ok(())
}
}