1use crate::config::VanConfig;
2use anyhow::{bail, Context, Result};
3use serde_json::Value;
4use std::collections::HashMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8#[derive(Clone)]
10pub struct VanProject {
11 pub root: PathBuf,
12 pub config: VanConfig,
13}
14
15impl VanProject {
16 pub fn load(dir: &Path) -> Result<Self> {
18 let pkg_path = dir.join("package.json");
19 if !pkg_path.exists() {
20 bail!("No package.json found. Are you in a Van project directory?");
21 }
22 let pkg_raw =
23 fs::read_to_string(&pkg_path).context("Failed to read package.json")?;
24 let config: VanConfig =
25 serde_json::from_str(&pkg_raw).context("Failed to parse package.json")?;
26 Ok(Self {
27 root: dir.to_path_buf(),
28 config,
29 })
30 }
31
32 pub fn load_cwd() -> Result<Self> {
34 let cwd = std::env::current_dir()?;
35 Self::load(&cwd)
36 }
37
38 pub fn collect_files(&self) -> Result<HashMap<String, String>> {
42 let src_dir = self.src_dir();
43 if !src_dir.exists() {
44 bail!("No src/ directory found.");
45 }
46 let mut files = HashMap::new();
47 collect_files_recursive(&src_dir, &src_dir, &mut files)?;
48
49 let node_modules = self.root.join("node_modules");
50 if node_modules.exists() {
51 collect_node_modules(&node_modules, &mut files)?;
52 }
53
54 Ok(files)
55 }
56
57 pub fn load_data(&self, page_key: &str) -> Value {
61 let all = self.load_all_data();
62 if let Some(page_data) = all.get(page_key) {
63 page_data.clone()
64 } else {
65 all
66 }
67 }
68
69 pub fn load_all_data(&self) -> Value {
71 let data_path = self.root.join("data/index.json");
72 let content = match fs::read_to_string(&data_path) {
73 Ok(c) => c,
74 Err(_) => return Value::Object(Default::default()),
75 };
76 match serde_json::from_str(&content) {
77 Ok(v) => v,
78 Err(_) => Value::Object(Default::default()),
79 }
80 }
81
82 pub fn page_entries(&self, files: &HashMap<String, String>) -> Vec<String> {
84 find_van_files(files, "pages/")
85 }
86
87 pub fn component_entries(&self, files: &HashMap<String, String>) -> Vec<String> {
89 find_van_files(files, "components/")
90 }
91
92 pub fn src_dir(&self) -> PathBuf {
93 self.root.join("src")
94 }
95
96 pub fn pages_dir(&self) -> PathBuf {
97 self.root.join("src").join("pages")
98 }
99
100 pub fn dist_dir(&self) -> PathBuf {
101 self.root.join("dist")
102 }
103}
104
105fn collect_files_recursive(
108 dir: &Path,
109 base: &Path,
110 files: &mut HashMap<String, String>,
111) -> Result<()> {
112 for entry in fs::read_dir(dir)? {
113 let entry = entry?;
114 let path = entry.path();
115 if path.is_dir() {
116 collect_files_recursive(&path, base, files)?;
117 } else if is_source_file(&path) {
118 let rel = path
119 .strip_prefix(base)
120 .unwrap_or(&path)
121 .to_string_lossy()
122 .replace('\\', "/");
123 let content = fs::read_to_string(&path)
124 .with_context(|| format!("Failed to read {}", path.display()))?;
125 files.insert(rel, content);
126 }
127 }
128 Ok(())
129}
130
131fn collect_node_modules(
134 node_modules: &Path,
135 files: &mut HashMap<String, String>,
136) -> Result<()> {
137 for scope_entry in fs::read_dir(node_modules)? {
138 let scope_entry = scope_entry?;
139 let scope_name = scope_entry.file_name().to_string_lossy().to_string();
140 if !scope_name.starts_with('@') || !scope_entry.path().is_dir() {
141 continue;
142 }
143 for pkg_entry in fs::read_dir(scope_entry.path())? {
144 let pkg_entry = pkg_entry?;
145 if !pkg_entry.path().is_dir() {
146 continue;
147 }
148 let pkg_dir = pkg_entry.path();
149 let pkg_name = pkg_dir.file_name().unwrap().to_string_lossy().to_string();
150 collect_scoped_package_recursive(&pkg_dir, &scope_name, &pkg_name, &pkg_dir, files)?;
151 }
152 }
153 Ok(())
154}
155
156fn collect_scoped_package_recursive(
157 dir: &Path,
158 scope_name: &str,
159 pkg_name: &str,
160 pkg_dir: &Path,
161 files: &mut HashMap<String, String>,
162) -> Result<()> {
163 for entry in fs::read_dir(dir)? {
164 let entry = entry?;
165 let path = entry.path();
166 if path.is_dir() {
167 collect_scoped_package_recursive(&path, scope_name, pkg_name, pkg_dir, files)?;
168 } else if is_source_file(&path) {
169 let rel = path
170 .strip_prefix(pkg_dir)
171 .unwrap_or(&path)
172 .to_string_lossy()
173 .replace('\\', "/");
174 let key = format!("{}/{}/{}", scope_name, pkg_name, rel);
175 let content = fs::read_to_string(&path)?;
176 files.insert(key, content);
177 }
178 }
179 Ok(())
180}
181
182fn is_source_file(path: &Path) -> bool {
183 matches!(
184 path.extension().and_then(|e| e.to_str()),
185 Some("van" | "ts" | "js")
186 )
187}
188
189fn find_van_files(files: &HashMap<String, String>, prefix: &str) -> Vec<String> {
190 let mut entries: Vec<String> = files
191 .keys()
192 .filter(|k| k.starts_with(prefix) && k.ends_with(".van"))
193 .cloned()
194 .collect();
195 entries.sort();
196 entries
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_is_source_file() {
205 assert!(is_source_file(Path::new("foo.van")));
206 assert!(is_source_file(Path::new("bar.ts")));
207 assert!(is_source_file(Path::new("baz.js")));
208 assert!(!is_source_file(Path::new("readme.md")));
209 assert!(!is_source_file(Path::new("style.css")));
210 }
211
212 #[test]
213 fn test_find_van_files() {
214 let mut files = HashMap::new();
215 files.insert("pages/index.van".into(), String::new());
216 files.insert("pages/about.van".into(), String::new());
217 files.insert("components/header.van".into(), String::new());
218 files.insert("utils/format.ts".into(), String::new());
219
220 let pages = find_van_files(&files, "pages/");
221 assert_eq!(pages, vec!["pages/about.van", "pages/index.van"]);
222
223 let components = find_van_files(&files, "components/");
224 assert_eq!(components, vec!["components/header.van"]);
225 }
226}