/// DreamStack CLI — the compiler command-line interface. /// /// Usage: /// dreamstack build — compile to HTML+JS /// dreamstack dev — dev server with hot reload /// dreamstack check — analyze without emitting use clap::{Parser, Subcommand}; use std::fs; use std::path::{Path, PathBuf}; #[derive(Parser)] #[command(name = "dreamstack")] #[command(about = "The DreamStack UI compiler", version = "0.1.0")] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Compile a .ds file to HTML+JS Build { /// Input .ds file file: PathBuf, /// Output directory (default: dist/) #[arg(short, long, default_value = "dist")] output: PathBuf, }, /// Start a dev server with hot reload Dev { /// Input .ds file file: PathBuf, /// Port to serve on #[arg(short, long, default_value_t = 3000)] port: u16, }, /// Type-check and analyze without compiling Check { /// Input .ds file file: PathBuf, }, } fn main() { let cli = Cli::parse(); match cli.command { Commands::Build { file, output } => cmd_build(&file, &output), Commands::Dev { file, port } => cmd_dev(&file, port), Commands::Check { file } => cmd_check(&file), } } fn compile(source: &str) -> Result { // 1. Lex let mut lexer = ds_parser::Lexer::new(source); let tokens = lexer.tokenize(); // Check for lexer errors for tok in &tokens { if let ds_parser::TokenKind::Error(msg) = &tok.kind { return Err(format!("Lexer error at line {}: {}", tok.line, msg)); } } // 2. Parse let mut parser = ds_parser::Parser::new(tokens); let program = parser.parse_program().map_err(|e| e.to_string())?; // 3. Analyze let graph = ds_analyzer::SignalGraph::from_program(&program); let views = ds_analyzer::SignalGraph::analyze_views(&program); // 4. Codegen let html = ds_codegen::JsEmitter::emit_html(&program, &graph, &views); Ok(html) } fn cmd_build(file: &Path, output: &Path) { println!("🔨 DreamStack build"); println!(" source: {}", file.display()); let source = match fs::read_to_string(file) { Ok(s) => s, Err(e) => { eprintln!("❌ Could not read {}: {}", file.display(), e); std::process::exit(1); } }; match compile(&source) { Ok(html) => { fs::create_dir_all(output).unwrap(); let out_path = output.join("index.html"); fs::write(&out_path, &html).unwrap(); println!(" output: {}", out_path.display()); println!("✅ Build complete! ({} bytes)", html.len()); println!(""); println!(" Open in browser:"); println!(" file://{}", fs::canonicalize(&out_path).unwrap().display()); } Err(e) => { eprintln!("❌ Compile error: {e}"); std::process::exit(1); } } } fn cmd_dev(file: &Path, port: u16) { println!("🚀 DreamStack dev server"); println!(" watching: {}", file.display()); println!(" serving: http://localhost:{port}"); println!(""); let source = match fs::read_to_string(file) { Ok(s) => s, Err(e) => { eprintln!("❌ Could not read {}: {}", file.display(), e); std::process::exit(1); } }; let html = match compile(&source) { Ok(html) => html, Err(e) => { eprintln!("❌ Compile error: {e}"); std::process::exit(1); } }; // Simple HTTP server let server = tiny_http::Server::http(format!("0.0.0.0:{port}")).unwrap(); println!("✅ Server running at http://localhost:{port}"); println!(" Press Ctrl+C to stop"); println!(""); for request in server.incoming_requests() { // Re-compile on each request for dev mode (simple hot-reload) let current_html = if let Ok(src) = fs::read_to_string(file) { compile(&src).unwrap_or_else(|e| { format!("
Compile error:\n{e}
") }) } else { html.clone() }; let response = tiny_http::Response::from_string(¤t_html) .with_header( tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"text/html; charset=utf-8"[..]) .unwrap(), ); let _ = request.respond(response); } } fn cmd_check(file: &Path) { println!("🔍 DreamStack check"); println!(" file: {}", file.display()); let source = match fs::read_to_string(file) { Ok(s) => s, Err(e) => { eprintln!("❌ Could not read {}: {}", file.display(), e); std::process::exit(1); } }; // Lex let mut lexer = ds_parser::Lexer::new(&source); let tokens = lexer.tokenize(); let mut errors = 0; for tok in &tokens { if let ds_parser::TokenKind::Error(msg) = &tok.kind { eprintln!(" ❌ Lexer error at line {}: {}", tok.line, msg); errors += 1; } } // Parse let mut parser = ds_parser::Parser::new(tokens); let program = match parser.parse_program() { Ok(p) => p, Err(e) => { eprintln!(" ❌ {}", e); std::process::exit(1); } }; // Analyze let graph = ds_analyzer::SignalGraph::from_program(&program); let views = ds_analyzer::SignalGraph::analyze_views(&program); println!(""); println!(" 📊 Signal Graph:"); for node in &graph.nodes { let kind_str = match &node.kind { ds_analyzer::SignalKind::Source => "source", ds_analyzer::SignalKind::Derived => "derived", ds_analyzer::SignalKind::Handler { event, .. } => "handler", }; let deps: Vec<&str> = node.dependencies.iter().map(|d| d.signal_name.as_str()).collect(); if deps.is_empty() { println!(" {} [{}]", node.name, kind_str); } else { println!(" {} [{}] ← depends on: {}", node.name, kind_str, deps.join(", ")); } } println!(""); println!(" 🖼️ Views:"); for view in &views { println!(" {} ({} bindings)", view.name, view.bindings.len()); for binding in &view.bindings { match &binding.kind { ds_analyzer::BindingKind::TextContent { signal } => { println!(" 📝 text bound to: {signal}"); } ds_analyzer::BindingKind::EventHandler { element_tag, event, .. } => { println!(" ⚡ {element_tag}.{event}"); } ds_analyzer::BindingKind::Conditional { condition_signals } => { println!(" ❓ conditional on: {}", condition_signals.join(", ")); } ds_analyzer::BindingKind::StaticContainer { kind, child_count } => { println!(" 📦 {kind} ({child_count} children)"); } ds_analyzer::BindingKind::StaticText { text } => { println!(" 📄 static: \"{text}\""); } } } } let topo = graph.topological_order(); println!(""); println!(" 🔄 Propagation order: {:?}", topo.iter().map(|&id| &graph.nodes[id].name).collect::>()); if errors == 0 { println!(""); println!("✅ No errors found"); } else { println!(""); eprintln!("❌ {} error(s) found", errors); std::process::exit(1); } }