Complete compiler pipeline from .ds source to reactive browser apps: - ds-parser: lexer (string interpolation, operators, keywords) + recursive descent parser with operator precedence + full AST types - ds-analyzer: signal graph extraction (source/derived classification), topological sort for glitch-free propagation, DOM binding analysis - ds-codegen: JavaScript emitter with embedded reactive runtime (~3KB signal/derived/effect system) and dark-theme CSS design system - ds-cli: build (compile to HTML+JS), dev (live server), check (analyze) Verified working: source signals, derived signals, event handlers, conditional rendering (when), 12 unit tests passing, 6.8KB output.
249 lines
7.7 KiB
Rust
249 lines
7.7 KiB
Rust
/// DreamStack CLI — the compiler command-line interface.
|
|
///
|
|
/// Usage:
|
|
/// dreamstack build <file.ds> — compile to HTML+JS
|
|
/// dreamstack dev <file.ds> — dev server with hot reload
|
|
/// dreamstack check <file.ds> — 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<String, String> {
|
|
// 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!("<html><body><pre style='color:red'>Compile error:\n{e}</pre></body></html>")
|
|
})
|
|
} 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::<Vec<_>>());
|
|
|
|
if errors == 0 {
|
|
println!("");
|
|
println!("✅ No errors found");
|
|
} else {
|
|
println!("");
|
|
eprintln!("❌ {} error(s) found", errors);
|
|
std::process::exit(1);
|
|
}
|
|
}
|