diff --git a/compiler/ds-cli/src/main.rs b/compiler/ds-cli/src/main.rs index 0765a0a..7570356 100644 --- a/compiler/ds-cli/src/main.rs +++ b/compiler/ds-cli/src/main.rs @@ -86,6 +86,11 @@ enum Commands { #[arg(short, long)] output: Option, }, + /// Initialize a new DreamStack project + Init { + /// Project name (creates directory; default: current dir) + name: Option, + }, } fn main() { @@ -99,6 +104,7 @@ fn main() { Commands::Playground { file, port } => cmd_playground(file.as_deref(), port), Commands::Add { name, list, all } => cmd_add(name, list, all), Commands::Convert { name, shadcn, output } => cmd_convert(&name, shadcn, output.as_deref()), + Commands::Init { name } => cmd_init(name), } } @@ -1310,6 +1316,96 @@ fn add_component(name: &str, dest: &Path, added: &mut std::collections::HashSet< // ── Converter: dreamstack convert ── +fn cmd_init(name: Option) { + let project_dir = match &name { + Some(n) => PathBuf::from(n), + None => std::env::current_dir().expect("Failed to get current directory"), + }; + + if name.is_some() { + fs::create_dir_all(&project_dir).expect("Failed to create project directory"); + } + + let components_dir = project_dir.join("components"); + fs::create_dir_all(&components_dir).expect("Failed to create components/ directory"); + + // Write starter app.ds + let app_source = r#"-- My DreamStack App + +let count = 0 +let name = "" + +view main = column [ + + -- Header + text "🚀 My App" { variant: "title" } + text "Built with DreamStack" { variant: "subtitle" } + + -- Stats + row [ + column [ + text "Users" { variant: "subtitle" } + text "1,247" { variant: "title" } + ] { variant: "card" } + + column [ + text "Revenue" { variant: "subtitle" } + text "$8,420" { variant: "title" } + ] { variant: "card" } + ] + + -- Input + text "Your name" { variant: "label" } + input { bind: name, placeholder: "Type here..." } + text "Hello, {name}!" + + -- Counter + row [ + button "Count: {count}" { click: count += 1, variant: "primary" } + button "Reset" { click: count = 0, variant: "ghost" } + ] +] +"#; + fs::write(project_dir.join("app.ds"), app_source).expect("Failed to write app.ds"); + + // Write dreamstack.json + let project_name = name.as_deref().unwrap_or("my-dreamstack-app"); + let config = format!(r#"{{ + "name": "{}", + "version": "0.1.0", + "entry": "app.ds" +}} +"#, project_name); + fs::write(project_dir.join("dreamstack.json"), config).expect("Failed to write dreamstack.json"); + + // Add starter components from registry + let starter_components = ["button", "card", "badge", "input"]; + for comp_name in &starter_components { + if let Some(item) = REGISTRY.iter().find(|r| r.name == *comp_name) { + let comp_path = components_dir.join(format!("{}.ds", comp_name)); + fs::write(&comp_path, item.source).expect("Failed to write component"); + } + } + + let display_name = name.as_deref().unwrap_or("."); + println!("🚀 DreamStack project initialized in {}/\n", display_name); + println!(" Created:"); + println!(" app.ds — your main application"); + println!(" dreamstack.json — project config"); + println!(" components/button.ds — button component"); + println!(" components/card.ds — card component"); + println!(" components/badge.ds — badge component"); + println!(" components/input.ds — input component\n"); + println!(" Next steps:"); + if name.is_some() { + println!(" cd {}", display_name); + } + println!(" dreamstack build app.ds -o dist"); + println!(" dreamstack dev app.ds"); + println!(" dreamstack add --list # see all 11 components"); + println!(" dreamstack add dialog # add with deps\n"); +} + fn cmd_convert(name: &str, shadcn: bool, output: Option<&Path>) { let tsx_source = if shadcn { // Fetch from shadcn/ui GitHub diff --git a/compiler/ds-codegen/src/js_emitter.rs b/compiler/ds-codegen/src/js_emitter.rs index 8a2eb3d..4b4eb74 100644 --- a/compiler/ds-codegen/src/js_emitter.rs +++ b/compiler/ds-codegen/src/js_emitter.rs @@ -771,6 +771,38 @@ impl JsEmitter { anchor_var } + // Each loop: `each item in list -> template` + Expr::Each(item_name, list_expr, body) => { + let container_var = self.next_node_id(); + let iter_js = self.emit_expr(list_expr); + let iter_var = self.next_node_id(); + + self.emit_line(&format!("const {} = document.createElement('div');", container_var)); + self.emit_line(&format!("{}.className = 'ds-each-list';", container_var)); + + self.emit_line("DS.effect(() => {"); + self.indent += 1; + self.emit_line(&format!("const {} = {};", iter_var, iter_js)); + self.emit_line(&format!("{}.innerHTML = '';", container_var)); + + self.emit_line(&format!( + "const __list = ({0} && {0}.value !== undefined) ? {0}.value : (Array.isArray({0}) ? {0} : []);", + iter_var + )); + self.emit_line(&format!("__list.forEach(({item_name}, _idx) => {{")); + self.indent += 1; + self.push_scope(&[item_name.as_str()]); + let child_var = self.emit_view_expr(body, graph); + self.emit_line(&format!("{}.appendChild({});", container_var, child_var)); + self.pop_scope(); + self.indent -= 1; + self.emit_line("});"); + self.indent -= 1; + self.emit_line("});"); + + container_var + } + // ForIn reactive list: `for item in items -> body` Expr::ForIn { item, index, iter, body } => { let container_var = self.next_node_id(); diff --git a/compiler/ds-parser/src/ast.rs b/compiler/ds-parser/src/ast.rs index 21917fe..a5229d7 100644 --- a/compiler/ds-parser/src/ast.rs +++ b/compiler/ds-parser/src/ast.rs @@ -263,6 +263,9 @@ pub enum Expr { Container(Container), /// When conditional: `when count > 10 -> ...` When(Box, Box), + + /// Each loop: `each item in list => template` + Each(String, Box, Box), /// Match expression Match(Box, Vec), /// Pipe: `expr | operator` diff --git a/compiler/ds-parser/src/lexer.rs b/compiler/ds-parser/src/lexer.rs index 690c448..0a1ee70 100644 --- a/compiler/ds-parser/src/lexer.rs +++ b/compiler/ds-parser/src/lexer.rs @@ -27,6 +27,8 @@ pub enum TokenKind { Effect, On, When, + Each, + InKw, Match, If, Then, @@ -306,6 +308,8 @@ impl Lexer { "effect" => TokenKind::Effect, "on" => TokenKind::On, "when" => TokenKind::When, + "each" => TokenKind::Each, + "in" => TokenKind::InKw, "match" => TokenKind::Match, "if" => TokenKind::If, "then" => TokenKind::Then, diff --git a/compiler/ds-parser/src/parser.rs b/compiler/ds-parser/src/parser.rs index 2efb207..99ef0bf 100644 --- a/compiler/ds-parser/src/parser.rs +++ b/compiler/ds-parser/src/parser.rs @@ -894,7 +894,17 @@ impl Parser { Ok(Expr::When(Box::new(cond), Box::new(body))) } - // Match + // Each loop: `each item in list => template` + TokenKind::Each => { + self.advance(); + let item_name = self.expect_ident()?; + self.expect(&TokenKind::InKw)?; + let list_expr = self.parse_primary()?; + self.expect(&TokenKind::Arrow)?; + self.skip_newlines(); + let body = self.parse_expr()?; + Ok(Expr::Each(item_name, Box::new(list_expr), Box::new(body))) + } TokenKind::Match => { self.advance(); let scrutinee = self.parse_primary()?; diff --git a/compiler/ds-types/src/checker.rs b/compiler/ds-types/src/checker.rs index 0fd859c..e1245dc 100644 --- a/compiler/ds-types/src/checker.rs +++ b/compiler/ds-types/src/checker.rs @@ -670,6 +670,11 @@ impl TypeChecker { self.infer_expr(body) } + Expr::Each(_, list, body) => { + let _ = self.infer_expr(list); + self.infer_expr(body) + } + Expr::ComponentUse { props, children, .. } => { for (_, val) in props { self.infer_expr(val); diff --git a/examples/each-demo.ds b/examples/each-demo.ds new file mode 100644 index 0000000..e0d052b --- /dev/null +++ b/examples/each-demo.ds @@ -0,0 +1,17 @@ +-- Each Loop Demo +-- Demonstrates list rendering with each ... in ... -> syntax + +let todos = ["Buy groceries", "Write DreamStack docs", "Ship v1.0", "Deploy to prod"] + +view main = column [ + text "📋 Todo List" { variant: "title" } + text "4 items" { variant: "subtitle" } + + each todo in todos -> + row [ + text "•" + text todo + ] + + button "Done" { variant: "primary" } +]