Skip to main content

van_context/
project.rs

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/// A loaded Van project, providing file collection and data utilities.
9#[derive(Clone)]
10pub struct VanProject {
11    pub root: PathBuf,
12    pub config: VanConfig,
13}
14
15impl VanProject {
16    /// Load a Van project from the given directory.
17    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    /// Load a Van project from the current working directory.
33    pub fn load_cwd() -> Result<Self> {
34        let cwd = std::env::current_dir()?;
35        Self::load(&cwd)
36    }
37
38    /// Collect all source files (.van, .ts, .js) from `src/` and `node_modules/@scope/`.
39    ///
40    /// Returns a HashMap keyed by relative path (e.g. `"pages/index.van"`).
41    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    /// Load page-specific data from `data/index.json`.
58    ///
59    /// Tries page-specific key first (e.g. `"pages/index"`), falls back to root object.
60    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    /// Load all data from `data/index.json`.
70    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    /// Find all page entries (files under `pages/` with `.van` extension).
83    pub fn page_entries(&self, files: &HashMap<String, String>) -> Vec<String> {
84        find_van_files(files, "pages/")
85    }
86
87    /// Find all component entries (files under `components/` with `.van` extension).
88    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
105/// Recursively collect source files (.van, .ts, .js) into the map.
106/// Keys are relative to `base` (e.g. `pages/index.van`).
107fn 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
131/// Collect scoped package files from `node_modules/@scope/` directories.
132/// Keys are like `@van-ui/button/button.van`.
133fn 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}