1use crate::{TOOLCHAIN, VERSION, configure_ai, read, run, write};
2use ravenrs_project::{Project, ProjectFile};
3use std::path::Path;
4use toml_edit::{DocumentMut, Item, Value, value};
5
6const WORKSPACE: &str = include_str!("../../../Cargo.toml");
7macro_rules! example {
8 ($name:literal) => {
9 (
10 include_str!(concat!("../../../examples/", $name, "/Cargo.toml")),
11 include_str!(concat!("../../../examples/", $name, "/src/lib.rs")),
12 )
13 };
14}
15
16pub fn project_files(name: &str, template: &str, target: &str) -> Result<Vec<ProjectFile>, String> {
18 if !valid_name(name) {
19 return Err("Nome inválido: use uma letra, seguida de letras ASCII, números, - ou _.".into());
20 }
21 if !matches!(target, "desktop" | "web" | "both") {
22 return Err("Alvo inválido: desktop, web ou both.".into());
23 }
24 let (manifest, source) = match template {
25 "hello" => example!("hello-world"),
26 "app" => example!("tutorial-tasks"),
27 "app-sqlite" => example!("tasks-sqlite"),
28 "relatorios" => example!("report-designer"),
29 _ => return Err("Modelo inválido: hello, app, app-sqlite ou relatorios.".into()),
30 };
31 let project = Project::new(name).source(source);
32 let normalized = project.crate_name().replace('-', "_");
33 let krate = name.replace('-', "_");
34 let mut files = project.files_from_app();
35 for file in &mut files {
37 let suffix = match file.path.as_str() {
38 "src/main.rs" => Some((format!("{normalized}::app()"), format!("{krate}::app()"))),
39 "web/index.html" => Some((format!("pkg/{normalized}.js"), format!("pkg/{krate}.js"))),
40 "README.md" => Some((format!("/{normalized}.wasm"), format!("/{krate}.wasm"))),
41 _ => None,
42 };
43 if let Some((from, to)) = suffix {
44 file.contents = file.contents.replace(&from, &to);
45 }
46 }
47 let mut manifest: DocumentMut = manifest.parse().map_err(|e| format!("Modelo: {e}"))?;
48 let workspace: DocumentMut = WORKSPACE.parse().map_err(|e| format!("Workspace: {e}"))?;
49 manifest["package"]["name"] = value(name);
50 manifest["package"]["version"] = value("0.1.0");
51 for key in ["edition", "rust-version", "license"] {
52 if manifest["package"][key].get("workspace").and_then(Item::as_bool) == Some(true) {
53 manifest["package"][key] = workspace["workspace"]["package"][key].clone();
54 }
55 }
56 manifest.as_table_mut().remove("lints");
57 manifest.as_table_mut().remove("dev-dependencies");
58 resolve_dependencies(&mut manifest["dependencies"], &workspace)?;
59 if let Some(targets) = manifest.get_mut("target").and_then(Item::as_table_mut) {
60 for (_, item) in targets.iter_mut() {
61 resolve_dependencies(&mut item["dependencies"], &workspace)?;
62 }
63 if target == "desktop" {
64 targets.remove("cfg(target_arch = \"wasm32\")");
65 }
66 }
67 let manifest = format!("# Generated by cargo-ravenrs {VERSION}\n{manifest}");
68 for file in &mut files {
69 if file.path == "Cargo.toml" {
70 file.contents = manifest.clone();
71 }
72 if file.path == "README.md" {
73 file.contents = file.contents.replace(
74 "Made in the RavenRS playground.",
75 &format!("Generated by cargo-ravenrs {VERSION} ({template}, {target})."),
76 );
77 }
78 }
79 if target == "desktop" {
80 files.retain(|f| f.path != "web/index.html");
81 }
82 let mut toolchain: DocumentMut = TOOLCHAIN.parse().map_err(|e| format!("Toolchain: {e}"))?;
83 if target == "desktop" {
84 toolchain["toolchain"].as_table_mut().ok_or("toolchain inválida")?.remove("targets");
85 }
86 files.push(ProjectFile {
87 path: "rust-toolchain.toml".into(),
88 contents: format!("# cargo-ravenrs {VERSION}\n{toolchain}"),
89 });
90 files.push(ProjectFile {
91 path: "tests/screen.rs".into(),
92 contents: format!(
93 r#"// cargo-ravenrs {VERSION}
94#[test]
95fn main_screen_renders() {{
96 use ravenrs::prelude::*;
97 let mut ui = Ui::new({krate}::app(), Theme::raven_dark());
98 ui.set_viewport(ravenrs::core::Size::new(1200.0, 800.0), 1.0);
99 ui.update();
100 assert!(!ui.scene().primitives().is_empty());
101}}
102"#,
103 krate = name.replace('-', "_")
104 ),
105 });
106 Ok(files)
107}
108
109fn valid_name(name: &str) -> bool {
110 const RESERVED: &[&str] = &[
111 "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if",
112 "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
113 "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "async",
114 "await", "dyn", "abstract", "become", "box", "do", "final", "gen", "macro", "override", "priv",
115 "typeof", "unsized", "virtual", "yield", "try", "test", "lib", "std", "core", "alloc", "con", "prn",
116 "aux", "nul",
117 ];
118 let lower = name.to_ascii_lowercase();
119 name.as_bytes().first().is_some_and(u8::is_ascii_alphabetic)
120 && name.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
121 && !RESERVED.contains(&name)
122 && !RESERVED.contains(&lower.as_str())
123 && !(lower.len() == 4
124 && (lower.starts_with("com") || lower.starts_with("lpt"))
125 && lower.as_bytes()[3].is_ascii_digit())
126}
127
128fn resolve_dependencies(item: &mut Item, workspace: &DocumentMut) -> Result<(), String> {
129 let Some(deps) = item.as_table_mut() else {
130 return Ok(());
131 };
132 for (name, dependency) in deps.iter_mut() {
133 if dependency.get("workspace").and_then(Item::as_bool) != Some(true) {
134 continue;
135 }
136 let original = dependency.clone();
137 *dependency = workspace["workspace"]["dependencies"]
138 .get(&*name)
139 .ok_or_else(|| format!("Dependência ausente: {name}"))?
140 .clone();
141 if let Some(table) = dependency.as_inline_table_mut() {
142 table.remove("path");
143 if let Some(extra) = original.get("features").and_then(Item::as_array) {
144 let features = table
145 .entry("features")
146 .or_insert(Value::Array(Default::default()))
147 .as_array_mut()
148 .ok_or("features inválidas")?;
149 for feature in extra.iter() {
150 features.push(feature.clone());
151 }
152 }
153 } else if let Some(version) = dependency.as_str() {
154 let mut table = toml_edit::InlineTable::new();
155 table.insert("version", version.into());
156 if let Some(features) = original.get("features").and_then(Item::as_value) {
157 table.insert("features", features.clone());
158 }
159 *dependency = Item::Value(Value::InlineTable(table));
160 }
161 if name.starts_with("ravenrs") {
162 dependency["version"] = value(format!("={VERSION}"));
163 }
164 }
165 Ok(())
166}
167
168pub fn new_project(
170 path: &Path,
171 template: &str,
172 target: &str,
173 git: bool,
174 ai: bool,
175 check: bool,
176 plugin: bool,
177) -> Result<(), String> {
178 let name = path.file_name().and_then(|s| s.to_str()).ok_or("Nome de projeto inválido")?;
179 let files = project_files(name, template, target)?;
180 if path.exists() {
181 let expected = format!("# Generated by cargo-ravenrs {VERSION}");
183 if !path.join("Cargo.toml").is_file() || !read(&path.join("Cargo.toml"))?.starts_with(&expected) {
184 return Err(format!("{} já existe e não foi gerado por esta versão.", path.display()));
185 }
186 let state: DocumentMut = read(&path.join(".ravenrs-project.toml"))?
187 .parse()
188 .map_err(|e| format!("Metadados do projeto inválidos: {e}"))?;
189 let previous_template =
190 state.get("template").and_then(Item::as_str).ok_or("Modelo original ausente nos metadados")?;
191 let previous_target =
192 state.get("target").and_then(Item::as_str).ok_or("Alvo original ausente nos metadados")?;
193 if previous_template != template || previous_target != target {
194 return Err(format!(
195 "{} já contém um projeto {previous_template}/{previous_target}; você escolheu {template}/{target}. Use outra pasta para criar esse modelo/alvo. O projeto existente não foi alterado.",
196 path.display()
197 ));
198 }
199 println!("Projeto existente: preservando o código.");
200 } else {
201 std::fs::create_dir_all(path).map_err(|e| e.to_string())?;
202 for file in files {
203 write(path, &file.path, &file.contents)?;
204 }
205 write(
206 path,
207 ".ravenrs-project.toml",
208 &format!(
209 "# cargo-ravenrs {VERSION}\nversion = \"{VERSION}\"\ntemplate = \"{template}\"\ntarget = \"{target}\"\nai = {ai}\n"
210 ),
211 )?;
212 }
213 if git && !path.join(".git").exists() {
214 run(path, "git", &["init"])?;
215 }
216 if ai {
217 configure_ai(path, plugin)?;
218 }
219 if check {
220 if target != "web" {
221 run(path, "cargo", &["check"])?;
222 }
223 if target != "desktop" {
224 run(path, "cargo", &["check", "--target", "wasm32-unknown-unknown"])?;
225 }
226 }
227 crate::mcp::probe("cargo", &["ravenrs".into(), "mcp".into()], path)?;
228 let next = if target == "web" {
229 "cargo check --target wasm32-unknown-unknown # veja README.md para servir web/"
230 } else {
231 "cargo run"
232 };
233 println!("Pronto.\n cd {}\n {next}\n claude # ou code .\n cargo ravenrs doctor", path.display());
234 Ok(())
235}