/// Build command — compile .ds files to HTML+JS or Panel IR. use std::fs; use std::path::{Path, PathBuf}; use std::collections::HashSet; /// Compile error with source for diagnostic rendering. pub struct CompileError { pub message: String, pub source: Option, } impl std::fmt::Display for CompileError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.message) } } pub fn compile(source: &str, base_dir: &Path, minify: bool) -> 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(CompileError { message: format!("Lexer error at line {}: {}", tok.line, msg), source: None, }); } } // 2. Parse let mut parser = ds_parser::Parser::with_source(tokens, source); let mut program = parser.parse_program().map_err(|e| { let diag = ds_diagnostic::Diagnostic::from(e); CompileError { message: ds_diagnostic::render(&diag, source), source: Some(source.to_string()), } })?; // 3. Resolve imports — inline exported declarations from imported files resolve_imports(&mut program, base_dir).map_err(|e| CompileError { message: e, source: None })?; // 4. Analyze let graph = ds_analyzer::SignalGraph::from_program(&program); let views = ds_analyzer::SignalGraph::analyze_views(&program); // 5. Codegen let html = ds_codegen::JsEmitter::emit_html(&program, &graph, &views, minify); Ok(html) } /// Compile a DreamStack source file to Panel IR JSON for ESP32 LVGL panels. pub fn compile_panel_ir(source: &str, base_dir: &Path) -> Result { // 1. Lex let mut lexer = ds_parser::Lexer::new(source); let tokens = lexer.tokenize(); for tok in &tokens { if let ds_parser::TokenKind::Error(msg) = &tok.kind { return Err(CompileError { message: format!("Lexer error at line {}: {}", tok.line, msg), source: None, }); } } // 2. Parse let mut parser = ds_parser::Parser::with_source(tokens, source); let mut program = parser.parse_program().map_err(|e| { let diag = ds_diagnostic::Diagnostic::from(e); CompileError { message: ds_diagnostic::render(&diag, source), source: Some(source.to_string()), } })?; // 3. Resolve imports resolve_imports(&mut program, base_dir).map_err(|e| CompileError { message: e, source: None })?; // 4. Analyze let graph = ds_analyzer::SignalGraph::from_program(&program); // 5. Codegen → Panel IR let ir = ds_codegen::IrEmitter::emit_ir(&program, &graph); Ok(ir) } /// Resolve `import { X, Y } from "./file"` by parsing the imported file /// and inlining the matching `export`ed declarations. pub fn resolve_imports(program: &mut ds_parser::Program, base_dir: &Path) -> Result<(), String> { let mut imported_decls = Vec::new(); let mut seen_files: HashSet = HashSet::new(); for decl in &program.declarations { if let ds_parser::Declaration::Import(import) = decl { // Resolve the file path relative to base_dir let mut import_path = base_dir.join(&import.source); if !import_path.extension().map_or(false, |e| e == "ds") { import_path.set_extension("ds"); } let import_path = import_path.canonicalize().unwrap_or(import_path.clone()); if seen_files.contains(&import_path) { continue; // Skip duplicate imports } seen_files.insert(import_path.clone()); // Read and parse the imported file let imported_source = fs::read_to_string(&import_path) .map_err(|e| format!("Cannot import '{}': {}", import.source, e))?; let mut lexer = ds_parser::Lexer::new(&imported_source); let tokens = lexer.tokenize(); for tok in &tokens { if let ds_parser::TokenKind::Error(msg) = &tok.kind { return Err(format!("Lexer error in '{}' at line {}: {}", import.source, tok.line, msg)); } } let mut parser = ds_parser::Parser::new(tokens); let mut imported_program = parser.parse_program() .map_err(|e| format!("Parse error in '{}': {}", import.source, e))?; // Recursively resolve imports in the imported file let imported_dir = import_path.parent().unwrap_or(base_dir); resolve_imports(&mut imported_program, imported_dir)?; // Extract matching exports let names: HashSet<&str> = import.names.iter().map(|s| s.as_str()).collect(); for d in &imported_program.declarations { match d { ds_parser::Declaration::Export(name, inner) if names.contains(name.as_str()) => { imported_decls.push(*inner.clone()); } // Also include non-exported decls that exports depend on // (for now, include all let decls from the imported file) ds_parser::Declaration::Let(_) => { imported_decls.push(d.clone()); } _ => {} } } } } // Remove Import declarations and prepend imported decls program.declarations.retain(|d| !matches!(d, ds_parser::Declaration::Import(_))); let mut merged = imported_decls; merged.append(&mut program.declarations); program.declarations = merged; Ok(()) } pub fn cmd_build(file: &Path, output: &Path, minify: bool, target: &str) { println!("🔨 DreamStack build (target: {}){}", target, if minify { " (minified)" } else { "" }); 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); } }; let base_dir = file.parent().unwrap_or(Path::new(".")); match target { "panel" => { // Panel IR target — emit JSON for ESP32 LVGL runtime match compile_panel_ir(&source, base_dir) { Ok(ir) => { fs::create_dir_all(output).unwrap(); let out_path = output.join("app.ir.json"); fs::write(&out_path, &ir).unwrap(); println!(" output: {}", out_path.display()); println!("✅ Panel IR built ({} bytes)", ir.len()); } Err(e) => { eprintln!("❌ {}", e.message); std::process::exit(1); } } } _ => { // Default HTML target match compile(&source, base_dir, minify) { 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!("❌ {}", e.message); std::process::exit(1); } } } } }